diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000..423cf594 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,9 @@ +# cargo-audit reads this file directly from the repository root context. +# +# RUSTSEC-2026-0194 / RUSTSEC-2026-0195 are denial-of-service advisories in +# quick-xml. quick-xml is transitive only through self_update's GitHub backend +# (`self_update -> quick-xml`) and parses trusted GitHub release metadata fetched +# over TLS, not user-controlled XML. self_update 0.44 still constrains quick-xml +# to ^0.38; remove these exceptions as soon as it accepts quick-xml >=0.41. +[advisories] +ignore = ["RUSTSEC-2026-0194", "RUSTSEC-2026-0195"] diff --git a/.githooks/pre-push b/.githooks/pre-push index 16856570..931c05cd 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,59 +1,8 @@ #!/usr/bin/env bash -# -# pre-push hook — push-time quality gate -# -# Runs ONLY on `git push`, never on `git commit`: commit freely to save WIP; -# the full gate runs only when you actually push. -# -# The checks mirror .github/workflows/ci.yml exactly, so "local pre-push green" -# implies "CI will be green": -# 1. cargo fmt --all --check (rustfmt) -# 2. cargo clippy ... -D warnings (lint, warnings are errors) -# 3. cargo test --workspace (incl. golden oracle + sync equivalence) -# 4. scripts/guardrail.sh (no AI/vector/LLM crates + no release -# asset-name drift) -# -# Enable once per clone: -# make hooks # == git config core.hooksPath .githooks -# +# Version-controlled pre-push gate. Keep this as a thin call to the canonical +# Makefile contract so local pushes, CI, and release verification cannot drift. set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel)" -cd "$REPO_ROOT" - -echo "==> [pre-push] running push gate (fmt → clippy → test → guardrail)..." - -echo "==> [1/4] cargo fmt --all --check" -if ! cargo fmt --all --check; then - echo "" >&2 - echo "❌ Format check failed. Run 'cargo fmt --all' (and 'make fmt' for docs)," >&2 - echo " commit the result, then push again. Push aborted." >&2 - exit 1 -fi - -echo "==> [2/4] cargo clippy --workspace --all-targets -- -D warnings" -if ! cargo clippy --workspace --all-targets -- -D warnings; then - echo "" >&2 - echo "❌ Clippy failed (warnings are denied). Fix the lints above and push again." >&2 - echo " Push aborted." >&2 - exit 1 -fi - -echo "==> [3/4] cargo test --workspace" -if ! cargo test --workspace; then - echo "" >&2 - echo "❌ Tests failed. Fix the failing tests above and push again. Push aborted." >&2 - exit 1 -fi - -echo "==> [4/4] scripts/guardrail.sh (no AI/vector/LLM crates + asset-name drift)" -if ! bash scripts/guardrail.sh; then - echo "" >&2 - echo "❌ Scope guardrail failed: a forbidden AI/vector/LLM crate was detected," >&2 - echo " or the release workflow and the installers disagree on asset names." >&2 - echo " Read the diagnostics above; push aborted." >&2 - exit 1 -fi - -echo "==> [pre-push] ✅ all checks passed, push allowed." -exit 0 +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" +echo "==> [pre-push] make pre-ci" +exec make pre-ci diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..4588e8d0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Routing only while the repository has one maintainer. The ruleset must not +# require code-owner approval until a second maintainer can review changes. +* @sunerpy +/.github/workflows/ @sunerpy +/.github/scripts/ @sunerpy +/scripts/ @sunerpy +/reference/golden/ @sunerpy diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..95408a5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,59 @@ +name: Bug report +description: Report incorrect indexing, querying, CLI, MCP, installer, or runtime behavior +title: "bug: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Do not report vulnerabilities here. Use the private channel in SECURITY.md. + - type: input + id: version + attributes: + label: CodeGraph version + description: Output of `codegraph --version` and installation method. + validations: + required: true + - type: input + id: platform + attributes: + label: Platform + placeholder: OS, architecture, filesystem, shell + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: Include the smallest source tree and exact commands. Remove secrets. + render: shell + validations: + required: true + - type: textarea + id: observed + attributes: + label: Observed behavior + description: Include complete error text and whether the result is deterministic. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: status + attributes: + label: Index status and diagnostics + description: Paste `codegraph status --json`; attach opt-in diagnostics when relevant. + render: json + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched existing issues and the upstream ledger. + required: true + - label: This report contains no secrets or confidential vulnerability details. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..1ae7335a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/sunerpy/codegraph-rust/security/advisories/new + about: Report vulnerabilities privately; do not open a public issue. + - name: Documentation + url: https://github.com/sunerpy/codegraph-rust/blob/main/docs/README.md + about: Read the CLI, MCP, language, architecture, and troubleshooting references. diff --git a/.github/ISSUE_TEMPLATE/extraction.yml b/.github/ISSUE_TEMPLATE/extraction.yml new file mode 100644 index 00000000..be2b5b7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/extraction.yml @@ -0,0 +1,67 @@ +name: Language or extraction gap +description: Report a missing, wrong, or fabricated symbol/edge for a minimal source fixture +title: "fix(extraction): " +labels: ["bug", "extraction"] +body: + - type: input + id: language + attributes: + label: Language and extension + placeholder: TypeScript (.ts) + validations: + required: true + - type: input + id: version + attributes: + label: CodeGraph version + description: Output of `codegraph --version`. + validations: + required: true + - type: textarea + id: fixture + attributes: + label: Minimal source fixture + description: Include every file needed to reproduce and no private code. + render: text + validations: + required: true + - type: textarea + id: commands + attributes: + label: Commands and observed graph + description: Show init/sync plus search, callers, export, or MCP calls that demonstrate the gap. + render: shell + validations: + required: true + - type: dropdown + id: problem_kind + attributes: + label: Problem kind + options: + - Missing symbol or edge + - Fabricated symbol or edge + - Wrong owner or target + - Wrong location or signature + - Import/path resolution + - Parser error or unsupported syntax + - Incremental/full-index mismatch + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected static-analysis result + description: Explain ambiguity and any dynamic behavior that should remain unresolved. + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I reduced this to a non-confidential fixture. + required: true + - label: I checked docs/languages.md for the documented boundary. + required: true + - label: I searched existing issues and the upstream ledger. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000..8c223bd9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,53 @@ +name: Feature request +description: Propose a new query, workflow, integration, or product behavior +title: "feat: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What user task cannot be completed reliably today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + description: Describe inputs, outputs, failure cases, and static/runtime boundary. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: dropdown + id: surface + attributes: + label: Primary surface + options: + - Extraction or resolution + - Graph or search + - CLI + - MCP + - Installer or agent integration + - Daemon or watcher + - Documentation or tooling + - Other + validations: + required: true + - type: checkboxes + id: compatibility + attributes: + label: Compatibility considerations + options: + - label: This may change graph output or require golden updates. + - label: This may change a public CLI, JSON, MCP, or installer contract. + - label: This may require a schema or extraction-version migration. + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched existing issues and the upstream ledger. + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..818df21f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 + +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + groups: + cargo: + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..9fc25c52 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ +## Summary + + + +## Evidence + + + +## Compatibility impact + +- [ ] No node/edge/reference/file-classification change +- [ ] Extraction version reviewed (state the decision below) +- [ ] Schema/migration impact reviewed +- [ ] CLI/MCP/installer output compatibility reviewed +- [ ] Golden changes are intentional and explained + +Details: + +## Validation + + + +- [ ] Focused tests +- [ ] `make check` +- [ ] `git diff --check` +- [ ] Documentation updated or not applicable + +## Risks and deferred work + + diff --git a/.github/scaffold.json b/.github/scaffold.json new file mode 100644 index 00000000..c227d167 --- /dev/null +++ b/.github/scaffold.json @@ -0,0 +1,25 @@ +{ + "profile": "rust", + "addons": ["codecov"], + "placeholders": { + "BINARY": "codegraph", + "CRATE": "codegraph-rs", + "CURRENT_VERSION": "0.50.3", + "DEFAULT_BRANCH": "main", + "OWNER": "sunerpy", + "PACKAGE_NAME": "codegraph-rs", + "REPO": "codegraph-rust", + "RUST_TOOLCHAIN": "1.96.0" + }, + "drift_allow": [ + ".github/workflows/ci.yml", + ".github/workflows/release.yml", + "release-please-config.json", + "Makefile", + "scripts/install.sh", + "scripts/install.ps1", + ".oxfmtignore", + "codecov.yml", + ".github/dependabot.yml" + ] +} diff --git a/.github/scripts/install-actionlint.sh b/.github/scripts/install-actionlint.sh new file mode 100755 index 00000000..ccc8cd2c --- /dev/null +++ b/.github/scripts/install-actionlint.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install the pinned actionlint release binary on a Linux x86_64 runner. +# +# taiki-e/install-action has no actionlint manifest, so the binary comes from +# the upstream GitHub release and is accepted only when its SHA-256 matches the +# digest pinned here. The version must equal ACTIONLINT_VERSION in the Makefile, +# which `make tools-check` enforces locally. +set -euo pipefail + +version="1.7.12" +sha256="8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" + +makefile_version="$(sed -n 's/^ACTIONLINT_VERSION := //p' Makefile)" +if [ "$makefile_version" != "$version" ]; then + printf 'install-actionlint: Makefile pins %s but this script pins %s\n' \ + "${makefile_version:-}" "$version" >&2 + exit 1 +fi + +archive="actionlint_${version}_linux_amd64.tar.gz" +dest="${RUNNER_TEMP:?}/actionlint-${version}" +mkdir -p "$dest" +curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$dest/$archive" \ + "https://github.com/rhysd/actionlint/releases/download/v${version}/${archive}" +printf '%s %s\n' "$sha256" "$dest/$archive" | sha256sum -c - +tar -xzf "$dest/$archive" -C "$dest" actionlint +"$dest/actionlint" -version +printf '%s\n' "$dest" >> "${GITHUB_PATH:?}" diff --git a/.github/scripts/publish-release-draft.sh b/.github/scripts/publish-release-draft.sh new file mode 100755 index 00000000..7639ba18 --- /dev/null +++ b/.github/scripts/publish-release-draft.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Verify the remote assets of a DRAFT GitHub Release and, on request, publish it. +# +# Usage: +# publish-release-draft.sh (--verify-only | --publish) [--checksums FILE] +# [--asset PATH]... [--expect-name NAME]... [--exact] +# +# Environment: GH_TOKEN (contents: write; drafts are invisible otherwise), REPO, TAG +# +# Verification (every listed expectation must hold, otherwise exit 1 and the +# draft is left untouched): +# --checksums FILE sha256sum-format lines; every named asset must exist remotely +# exactly once with digest sha256: +# --asset PATH the remote asset named basename(PATH) must have the local +# byte size and SHA-256 digest (repeatable) +# --expect-name NAME the remote asset must exist exactly once (repeatable) +# --exact the remote asset count must equal the expected set +# +# Draft-safe lookup: the release is resolved with `gh release view`, which falls +# back to GraphQL for drafts. Never use the REST path releases/tags/TAG for a +# draft; it returns only published releases (404 for the draft). +# +# --publish runs `gh release edit --draft=false`, adds `--latest` only when the +# release is not a prerelease (`--latest=false` otherwise), then re-reads the +# release and asserts draft == false. +# +# Immutable releases: `gh release upload --clobber` re-uploads work only while +# the release is still a draft; after the flip assets and the tag are locked. +# +# Sources: +# https://raw.githubusercontent.com/cli/cli/trunk/pkg/cmd/release/shared/fetch.go +# https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name +# https://docs.github.com/en/rest/releases/releases#update-a-release +# https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases +set -euo pipefail + +semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' + +fail() { + echo "::error title=Publication::$*" >&2 + exit 1 +} + +usage() { + sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +action= +checksums= +exact=0 +assets=() +expect_names=() +while (($# > 0)); do + case "$1" in + --verify-only | --publish) + [[ -z "$action" ]] || fail "choose exactly one of --verify-only or --publish" + action=${1#--} + shift + ;; + --checksums) + [[ $# -ge 2 ]] || fail "--checksums requires a file" + [[ -z "$checksums" ]] || fail "--checksums may be given once" + checksums=$2 + shift 2 + ;; + --asset) + [[ $# -ge 2 ]] || fail "--asset requires a path" + assets+=("$2") + shift 2 + ;; + --expect-name) + [[ $# -ge 2 ]] || fail "--expect-name requires a name" + expect_names+=("$2") + shift 2 + ;; + --exact) + exact=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done +[[ -n "$action" ]] || fail "one of --verify-only or --publish is required" +for name in GH_TOKEN REPO TAG; do + [[ -n "${!name:-}" ]] || fail "environment variable ${name} is required" +done +[[ "$TAG" =~ $semver ]] || fail "TAG must be a v-prefixed SemVer, got '${TAG}'" + +# --- resolve the draft -------------------------------------------------------- +release=$(gh release view "$TAG" --repo "$REPO" --json databaseId,isDraft,isPrerelease,tagName,assets) \ + || fail "no Release exists for ${TAG} in ${REPO}" +[[ "$(jq -r '.tagName' <<<"$release")" == "$TAG" ]] || fail "release lookup returned a different tag" +[[ "$(jq -r '.isDraft' <<<"$release")" == true ]] || fail "${TAG} is not a draft; refusing to touch a published release" +prerelease=$(jq -r '.isPrerelease' <<<"$release") +[[ "$prerelease" == true || "$prerelease" == false ]] || fail "isPrerelease is not boolean" + +remote_count=$(jq -r '.assets | length' <<<"$release") +duplicates=$(jq -r '[.assets[].name] | group_by(.) | map(select(length > 1) | .[0]) | .[]' <<<"$release") +[[ -z "$duplicates" ]] || fail "remote asset names are duplicated: ${duplicates//$'\n'/, }" + +# name -> "size digest state" for the remote assets +declare -A remote_size=() remote_digest=() remote_state=() +while IFS=$'\t' read -r name size digest state; do + remote_size["$name"]=$size + remote_digest["$name"]=$digest + remote_state["$name"]=$state +done < <(jq -r '.assets[] | [.name, (.size | tostring), (.digest // "null"), (.state // "null")] | @tsv' <<<"$release") + +declare -A expected=() +failures=0 +problem() { + echo "::error title=Publication::$*" >&2 + failures=$((failures + 1)) +} + +require_remote() { + local name=$1 + expected["$name"]=1 + if [[ -z "${remote_size[$name]+set}" ]]; then + problem "asset ${name} is missing from the draft" + return 1 + fi + if [[ "${remote_state[$name]}" != uploaded ]]; then + problem "asset ${name} is in state ${remote_state[$name]}, not uploaded" + return 1 + fi + if [[ "${remote_digest[$name]}" == null ]]; then + problem "asset ${name} has no server-side digest yet; re-run after GitHub finishes processing the upload" + return 1 + fi + return 0 +} + +if [[ -n "$checksums" ]]; then + [[ -r "$checksums" ]] || fail "checksum file is not readable: ${checksums}" + line_count=0 + while IFS= read -r line || [[ -n "$line" ]]; do + [[ -n "$line" ]] || continue + line_count=$((line_count + 1)) + if [[ ! "$line" =~ ^([0-9a-f]{64})[[:space:]][[:space:]\*]([^[:space:]].*)$ ]]; then + problem "checksum line is not sha256sum format: ${line}" + continue + fi + hex=${BASH_REMATCH[1]} + name=${BASH_REMATCH[2]} + require_remote "$name" || continue + if [[ "${remote_digest[$name]}" != "sha256:${hex}" ]]; then + problem "remote digest for ${name} is ${remote_digest[$name]}, expected sha256:${hex}" + fi + done <"$checksums" + ((line_count > 0)) || fail "checksum file ${checksums} lists no assets" +fi + +for path in ${assets[@]+"${assets[@]}"}; do + [[ -f "$path" ]] || fail "asset file is not readable: ${path}" + name=$(basename "$path") + require_remote "$name" || continue + size=$(stat -c '%s' "$path" 2>/dev/null || stat -f '%z' "$path") + digest="sha256:$(sha256sum "$path" | cut -d ' ' -f 1)" + if [[ "${remote_size[$name]}" != "$size" ]]; then + problem "remote size for ${name} is ${remote_size[$name]}, local file is ${size} bytes" + fi + if [[ "${remote_digest[$name]}" != "$digest" ]]; then + problem "remote bytes differ for ${name}: ${remote_digest[$name]} != ${digest}" + fi +done + +for name in ${expect_names[@]+"${expect_names[@]}"}; do + require_remote "$name" || true +done + +if ((exact)); then + ((${#expected[@]} > 0)) || fail "--exact needs at least one expectation (--checksums, --asset, or --expect-name)" + if ((remote_count != ${#expected[@]})); then + extra=$(jq -r --argjson expected "$(printf '%s\n' "${!expected[@]}" | jq -R . | jq -s .)" \ + '[.assets[].name] - $expected | .[]' <<<"$release") + problem "draft holds ${remote_count} assets but exactly ${#expected[@]} were expected; unexpected: ${extra//$'\n'/, }" + fi +fi + +((failures == 0)) || fail "${failures} remote asset check(s) failed; the draft is unchanged" +echo "verified ${#expected[@]} expected asset(s) on draft ${TAG} (${remote_count} remote, prerelease=${prerelease})" + +[[ "$action" == publish ]] || exit 0 + +# --- publish ------------------------------------------------------------------- +args=(release edit "$TAG" --repo "$REPO" --draft=false) +if [[ "$prerelease" == false ]]; then + args+=(--latest) +else + args+=(--latest=false) +fi +gh "${args[@]}" +after=$(gh release view "$TAG" --repo "$REPO" --json isDraft,isPrerelease,tagName) +[[ "$(jq -r '.tagName' <<<"$after")" == "$TAG" ]] || fail "post-publish lookup returned a different tag" +[[ "$(jq -r '.isDraft' <<<"$after")" == false ]] || fail "${TAG} is still a draft after the flip" +echo "published ${TAG} (prerelease=${prerelease}, latest=$([[ "$prerelease" == false ]] && echo true || echo false))" diff --git a/.github/scripts/resolve-release-gate.sh b/.github/scripts/resolve-release-gate.sh new file mode 100755 index 00000000..3505b95e --- /dev/null +++ b/.github/scripts/resolve-release-gate.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Resolve the release gate for the same-run release templates and expose the +# tag-peel helper the candidate controller uses. +# +# Usage (inside the release-please job, after actions/checkout): +# bash .github/scripts/resolve-release-gate.sh +# bash .github/scripts/resolve-release-gate.sh --peel-tag vX.Y.Z # prints the commit SHA +# +# Environment (all values arrive through `env:`, never inline in `run:`): +# EVENT_NAME github.event_name (push or workflow_dispatch) +# INPUT_TAG inputs.tag_name on workflow_dispatch: an existing DRAFT tag to rebuild +# RELEASE_CREATED release-please output release_created ("true" when a Release was created) +# RELEASE_TAG release-please output tag_name +# RELEASE_VERSION release-please output version (cross-checked against the tag) +# RELEASE_SHA release-please output sha (optional; cross-checked against the peeled tag) +# GH_TOKEN github.token; the job holds contents: write so drafts are visible +# REPO github.repository +# REF_PROTECTED github.ref_protected (required; must be "true") +# RUN_REF github.ref (defaults to GITHUB_REF) +# DEFAULT_BRANCH expected default branch (defaults to the repository's default branch via the API) +# GATE_PASSTHROUGH optional space-separated list of env names copied to GITHUB_OUTPUT with +# lowercase keys (for example "BUILD_BINARIES NPM_PUBLISH"), because job-level +# `if:` cannot read workflow env but can read needs..outputs.* +# +# Outputs (GITHUB_OUTPUT): build_artifacts, tag_name, version, prerelease, release_sha +# +# Trust rule: the job that runs this script holds a write token, so the script +# refuses to run unless the run is on the protected default branch. This mirrors +# the candidate controller's trust job. +# +# Sources: +# https://raw.githubusercontent.com/googleapis/release-please-action/main/README.md (outputs) +# https://raw.githubusercontent.com/cli/cli/trunk/pkg/cmd/release/shared/fetch.go +# (gh release view resolves drafts through GraphQL; REST releases/tags/TAG does not) +# https://docs.github.com/en/actions/reference/security/secure-use#principle-of-least-privilege +set -euo pipefail + +semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' + +fail() { + echo "::error title=Release gate::$*" >&2 + exit 1 +} + +require_env() { + local name + for name in "$@"; do + [[ -n "${!name:-}" ]] || fail "environment variable ${name} is required" + done +} + +require_semver_tag() { + [[ "$1" =~ $semver ]] || fail "tag must be a v-prefixed SemVer such as v1.2.3, got '${1}'" +} + +# Print the commit a tag points at, dereferencing annotated tag objects. +peel_tag() { + local tag=$1 ref object_type sha annotated hops=0 + ref=$(gh api "repos/${REPO}/git/ref/tags/${tag}") || fail "tag ${tag} does not exist in ${REPO}" + object_type=$(jq -r '.object.type' <<<"$ref") + sha=$(jq -r '.object.sha' <<<"$ref") + while [[ "$object_type" == tag ]]; do + hops=$((hops + 1)) + ((hops <= 5)) || fail "tag ${tag} nests more than five tag objects" + annotated=$(gh api "repos/${REPO}/git/tags/${sha}") + object_type=$(jq -r '.object.type' <<<"$annotated") + sha=$(jq -r '.object.sha' <<<"$annotated") + done + [[ "$object_type" == commit ]] || fail "tag ${tag} does not peel to a commit (${object_type})" + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || fail "tag ${tag} peeled to an invalid SHA" + printf '%s\n' "$sha" +} + +emit() { + local out=${GITHUB_OUTPUT:-/dev/stdout} + printf '%s\n' "$@" >>"$out" +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 +fi + +require_env GH_TOKEN REPO + +if [[ "${1:-}" == "--peel-tag" ]]; then + [[ $# -eq 2 ]] || fail "--peel-tag takes exactly one tag" + require_semver_tag "$2" + peel_tag "$2" + exit 0 +fi +[[ $# -eq 0 ]] || fail "unknown arguments: $*" + +# --- trust: protected default branch only ----------------------------------- +[[ -n "${REF_PROTECTED+set}" ]] || fail "set REF_PROTECTED: \${{ github.ref_protected }} in the step env" +run_ref=${RUN_REF:-${GITHUB_REF:-}} +[[ -n "$run_ref" ]] || fail "RUN_REF (github.ref) is required" +default_branch=${DEFAULT_BRANCH:-} +if [[ -z "$default_branch" ]]; then + default_branch=$(gh api "repos/${REPO}" --jq .default_branch) +fi +[[ "$REF_PROTECTED" == true ]] \ + || fail "the release gate runs only on a protected branch (github.ref_protected is '${REF_PROTECTED}'); protect ${default_branch} with a ruleset or branch protection" +[[ "$run_ref" == "refs/heads/${default_branch}" ]] \ + || fail "the release gate runs only from refs/heads/${default_branch}, not ${run_ref}" + +# --- gate -------------------------------------------------------------------- +require_env EVENT_NAME +case "$EVENT_NAME" in +workflow_dispatch) + [[ -n "${INPUT_TAG:-}" ]] || fail "workflow_dispatch requires the tag_name input" + require_semver_tag "$INPUT_TAG" + tag=$INPUT_TAG + draft_message="manual rebuilds require an existing draft Release for ${tag}" + ;; +push) + if [[ "${RELEASE_CREATED:-}" != true ]]; then + echo "no release was created by this push; downstream release jobs stay skipped" + emit "build_artifacts=false" "tag_name=" "version=" "prerelease=" "release_sha=" + exit 0 + fi + [[ -n "${RELEASE_TAG:-}" ]] || fail "release-please reported release_created without a tag_name" + require_semver_tag "$RELEASE_TAG" + tag=$RELEASE_TAG + draft_message="release-please must create the Release as a draft (set \"draft\": true in release-please-config.json)" + ;; +*) + fail "unsupported event ${EVENT_NAME}; the release gate runs on push and workflow_dispatch only" + ;; +esac + +release_sha=$(peel_tag "$tag") +if [[ -n "${RELEASE_SHA:-}" && "$RELEASE_SHA" != "$release_sha" ]]; then + fail "release-please reported sha ${RELEASE_SHA} but ${tag} peels to ${release_sha}" +fi +version=${tag#v} +if [[ -n "${RELEASE_VERSION:-}" && "$RELEASE_VERSION" != "$version" ]]; then + fail "release-please reported version ${RELEASE_VERSION} but the tag is ${tag}" +fi + +release=$(gh release view "$tag" --repo "$REPO" --json isDraft,isPrerelease,tagName) \ + || fail "no Release exists for ${tag}" +[[ "$(jq -r '.tagName' <<<"$release")" == "$tag" ]] || fail "Release lookup returned a different tag" +[[ "$(jq -r '.isDraft' <<<"$release")" == true ]] || fail "$draft_message" +prerelease=$(jq -r '.isPrerelease' <<<"$release") +[[ "$prerelease" == true || "$prerelease" == false ]] || fail "isPrerelease is not boolean" + +echo "release gate open: ${tag} (${version}) at ${release_sha}, prerelease=${prerelease}" +emit "build_artifacts=true" "tag_name=${tag}" "version=${version}" \ + "prerelease=${prerelease}" "release_sha=${release_sha}" + +for name in ${GATE_PASSTHROUGH:-}; do + [[ "$name" =~ ^[A-Z][A-Z0-9_]*$ ]] || fail "GATE_PASSTHROUGH names must match ^[A-Z][A-Z0-9_]*$: ${name}" + value=${!name-} + [[ "$value" != *$'\n'* ]] || fail "GATE_PASSTHROUGH value for ${name} must be single-line" + emit "${name,,}=${value}" +done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd117a25..bd5352a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,247 +2,309 @@ name: CI on: push: - branches: - - main + branches: [main] pull_request: - branches: - - main + branches: [main] + merge_group: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + env: CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: "1" + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: sccache jobs: - test: - name: Test - runs-on: ubuntu-latest - timeout-minutes: 30 + workspace-version: + name: Workspace Version + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 + + - name: Require the version gate's parser + run: command -v jq >/dev/null 2>&1 || { echo "jq is required"; exit 1; } + + # This is deliberately the workflow's first Cargo subprocess. Every + # Cargo-running job below depends on this job. + - name: Verify workspace version surfaces + run: bash scripts/check-workspace-versions.sh + + linux: + name: Linux + needs: workspace-version + runs-on: ubuntu-24.04 + timeout-minutes: 45 steps: - - name: Checkout code - uses: actions/checkout@v5 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 with: + toolchain: 1.96.0 components: rustfmt,clippy - - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + - name: Verify workspace version surfaces before Cargo/cache + run: bash scripts/check-workspace-versions.sh - - name: Format check (rustfmt) - run: cargo fmt --all --check + - name: Cache Rust inputs + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + cache-targets: false - - name: Clippy lint - run: cargo clippy --workspace --all-targets -- -D warnings + - name: Configure compiler cache + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: "v0.16.0" - - name: Run tests (incl. golden oracle + sync equivalence) - run: cargo test --workspace + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" - # Windows cross-check is intentionally omitted on Ubuntu: target C deps - # (tree-sitter/libsqlite3-sys/ring) require MSVC tooling. Runtime - # correctness is gated by the native windows-latest job below. + - name: Install pinned repository tools + run: npm install --global oxfmt@0.64.0 - # The guardrail's asset-name drift gate parses release-please.yml with - # yaml.safe_load, so PyYAML must be importable. Idempotent: installs only - # when the runner image does not already ship it. A missing PyYAML must - # never degrade the gate into a silent skip — the gate itself exits nonzero - # in that case, and this step fails loudly first. - - name: Ensure PyYAML (asset-name drift gate) + - name: Install pinned actionlint + run: bash .github/scripts/install-actionlint.sh + + - name: Require fixture parsers and shell linter run: | + set -euo pipefail + command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck is required"; exit 1; } python3 -c 'import yaml' \ - || python3 -m pip install --break-system-packages PyYAML \ - || python3 -m pip install PyYAML - - # The CI-gate integrity block of the guardrail runs the shipped `ci-success` - # step body over a synthetic needs context, which needs jq. Present on - # ubuntu-latest; asserted here so a missing jq fails loudly rather than - # letting the gate degrade. - - name: Ensure jq (CI gate-integrity check) - run: jq --version - - - name: Scope guardrail (no AI/vector/LLM crates + release asset-name drift + CI gate integrity) - run: bash scripts/guardrail.sh - - windows: - name: Windows - runs-on: windows-latest - timeout-minutes: 30 + || python3 -m pip install --break-system-packages PyYAML==6.0.3 \ + || python3 -m pip install PyYAML==6.0.3 + + - name: Run the complete local gate + run: make check + + windows-clippy: + name: Windows Clippy + needs: workspace-version + runs-on: windows-2025 + timeout-minutes: 35 + env: + CARGO_PROFILE_TEST_DEBUG: 0 + CARGO_PROFILE_TEST_SPLIT_DEBUGINFO: off steps: - - name: Checkout code - uses: actions/checkout@v5 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 with: - components: rustfmt,clippy + toolchain: 1.96.0 + components: clippy - - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + - name: Verify workspace version surfaces before Cargo/cache + shell: bash + run: bash scripts/check-workspace-versions.sh - - name: Clippy lint - # codegraph-bench is a Unix-only dev harness (publish=false); excluded from the Windows job. - run: cargo clippy --workspace --exclude codegraph-bench --all-targets -- -D warnings + - name: Cache Rust inputs + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + cache-targets: false - - name: Run tests - run: cargo test --workspace --exclude codegraph-bench + - name: Configure compiler cache + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: "v0.16.0" + + - name: Clippy + run: cargo clippy --workspace --exclude codegraph-bench --all-targets --locked -- -D warnings + + windows-test: + name: Windows Tests + needs: workspace-version + runs-on: windows-2025 + timeout-minutes: 40 + env: + CARGO_PROFILE_TEST_DEBUG: 0 + CARGO_PROFILE_TEST_SPLIT_DEBUGINFO: off + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 - # Batch M item 16 acceptance: ONE long-lived shipped `serve --mcp` process - # releases every SQLite handle when a request completes, so the main - # database can be REPLACED (MoveFileEx + REPLACE_EXISTING) without the - # compatibility close seam, and the next request serves only the - # replacement graph. The replacement rename is the Windows-specific handle - # proof (a retained handle makes it fail with a sharing violation), so this - # target is selected EXPLICITLY here — never compile-only, never skipped — - # even though the workspace run above already covers it. - - name: Run Batch M item 16 native Windows replacement acceptance - run: cargo test -p codegraph-rs --test batch_m_long_lived_mcp + - name: Verify workspace version surfaces before Cargo/cache + shell: bash + run: bash scripts/check-workspace-versions.sh + + - name: Parse the PowerShell installer + shell: pwsh + run: | + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + "scripts/install.ps1", [ref]$tokens, [ref]$errors + ) | Out-Null + if ($errors.Count -ne 0) { + $errors | ForEach-Object { Write-Error $_.ToString() } + exit 1 + } + + - name: Cache Rust inputs + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + cache-targets: false + + - name: Configure compiler cache + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: "v0.16.0" + + - name: Run native Windows tests + run: cargo test --workspace --exclude codegraph-bench --locked + + # A retained SQLite handle makes the replacement rename fail with a + # sharing violation, so this remains an explicit native acceptance gate. + - name: Run long-lived MCP database replacement acceptance + run: cargo test -p codegraph-rs --locked --test batch_m_long_lived_mcp audit: name: Security Audit - runs-on: ubuntu-latest - timeout-minutes: 10 + needs: workspace-version + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - - name: Checkout code - uses: actions/checkout@v5 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 + + - name: Verify workspace version surfaces before Cargo + run: bash scripts/check-workspace-versions.sh - name: Install cargo-audit - uses: taiki-e/install-action@v2 - with: - tool: cargo-audit - - - name: Run cargo-audit - # --ignore rationale documented in audit.toml. quick-xml (RUSTSEC-2026-0194/0195) - # is transitive via self_update and only parses trusted GitHub release metadata — - # not reachable; no self_update release pins quick-xml >= 0.41 yet. number_prefix - # (RUSTSEC-2025-0119) is an unmaintained-crate warning. - run: cargo audit --ignore RUSTSEC-2025-0119 --ignore RUSTSEC-2026-0194 --ignore RUSTSEC-2026-0195 - - # Coverage is INFORMATIONAL: it measures unit-test coverage and uploads to - # Codecov, but it is deliberately kept OUT of the `ci-success` gate's `needs:` - # so it can never block a merge. The Codecov status itself is also - # `informational: true` (see codecov.yml), so a below-target % (baseline - # ~72%, aspirational target 95%) never turns a PR red. This honors the iron - # rule "local green ⇒ CI green". + uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4 + with: + tool: cargo-audit@0.22.2 + + - name: Audit dependencies + run: cargo audit + + # Informational by policy: omitted from CI Success and informational in + # codecov.yml. A reporting outage or a below-target percentage cannot block. coverage: name: Coverage - runs-on: ubuntu-latest - timeout-minutes: 30 + needs: workspace-version + runs-on: ubuntu-24.04 + timeout-minutes: 45 steps: - - name: Checkout code - uses: actions/checkout@v5 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 with: + toolchain: 1.96.0 components: llvm-tools-preview - - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + - name: Verify workspace version surfaces before Cargo/cache + run: bash scripts/check-workspace-versions.sh + + - name: Cache Rust inputs + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + cache-targets: false - name: Install cargo-llvm-cov - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4 with: - tool: cargo-llvm-cov + tool: cargo-llvm-cov@0.8.7 - - name: Generate coverage (lcov) - run: cargo llvm-cov --workspace --lcov --output-path lcov.info --ignore-filename-regex 'codegraph-bench' + - name: Generate coverage + # Instrumented builds bypass the compiler cache: this job never + # configures sccache, so the workflow-level wrapper must not apply. + env: + RUSTC_WRAPPER: "" + SCCACHE_GHA_ENABLED: "false" + run: make coverage-lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: lcov.info - # Report-only: never fail CI on a Codecov upload/processing error. fail_ci_if_error: false - # Public repo: tokenless upload works. A token is optional here (kept - # for reliability) and REQUIRED for private repos. Add CODECOV_TOKEN - # to repo Secrets to use it; harmless/ignored if unset on a public repo. token: ${{ secrets.CODECOV_TOKEN }} - # ci-success is the SINGLE required status check for branch protection AND the - # job the release workflow's `verify-ci` waits on before a release may leave - # draft. It must therefore be a STRICT gate: it passes only when every required - # job's result is exactly `success`. - # - # WHY strict (the bug this shape fixes): `needs..result` can be `success`, - # `failure`, `cancelled`, or `skipped`. The previous check rejected only - # `failure`, so `cancelled` and `skipped` both PASSED. Combined with - # `cancel-in-progress: true` above, a run cancelled by a newer push could still - # conclude `CI Success` — and release-please.yml's `verify-ci` gates the release - # on exactly that job. A release could then be cut on a run whose tests never - # finished. Anything other than `success` now fails, including any result value - # GitHub may add in the future (unknown values are not silently tolerated). - # - # WHY the results are read from `toJSON(needs)` rather than named one by one: - # the step body hardcodes no job name, so adding a job to `needs:` below - # automatically makes it required with no change to the script. GitHub Actions - # cannot make this fully automatic — the `needs` context contains ONLY the jobs - # listed in `needs:`, so a new job that is never added there is invisible to - # this gate no matter how the expression is written. That gap is closed OUTSIDE - # the workflow, loudly: `scripts/check-ci-gate.sh` (run by scripts/guardrail.sh - # in `make ci`, the pre-push hook, and the CI `test` job) asserts that - # `ci-success.needs` equals every job in this file minus an explicit - # informational allow-list, so forgetting to list a new job turns CI red - # instead of silently narrowing the gate. ci-success: name: CI Success - runs-on: ubuntu-latest + needs: [workspace-version, linux, windows-clippy, windows-test, audit] if: always() - # NOTE: `coverage` is intentionally NOT listed here — coverage is - # informational (codecov.yml sets `informational: true`, baseline ~72% vs an - # aspirational 95% target) and must never gate a merge or a release. It is - # the ONLY allowed exclusion; scripts/check-ci-gate.sh enforces that, and - # also enforces that codecov.yml still marks coverage informational, so the - # exclusion can never outlive its justification. - needs: [test, audit, windows] + runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - - name: Check job status + - name: Require every CI job to succeed env: - # The whole needs context as JSON: {"test":{"result":"success",...},...} NEEDS_JSON: ${{ toJSON(needs) }} run: | set -euo pipefail - if [ -z "${NEEDS_JSON:-}" ]; then - echo "::error title=CI gate::the needs context was empty; cannot prove any required job succeeded — failing closed." + echo "::error title=CI gate::the needs context was empty; failing closed" exit 1 fi - if ! printf '%s' "$NEEDS_JSON" | jq -e 'type == "object" and length > 0' > /dev/null 2>&1; then - echo "::error title=CI gate::the needs context did not parse as a non-empty JSON object — failing closed." - printf 'needs context was: %s\n' "$NEEDS_JSON" + if ! printf '%s' "$NEEDS_JSON" | jq -e 'type == "object" and length > 0' >/dev/null 2>&1; then + echo "::error title=CI gate::the needs context was not a non-empty object; failing closed" exit 1 fi - - # One row per required job: "\t". A job whose `result` key - # is absent renders as and fails, never passes. mapfile -t rows < <( printf '%s' "$NEEDS_JSON" \ | jq -r 'to_entries | sort_by(.key)[] | "\(.key)\t\(.value.result // "")"' ) - if [ "${#rows[@]}" -eq 0 ]; then - echo "::error title=CI gate::no required jobs were found in the needs context — failing closed." - exit 1 - fi - - echo "Required job results (only 'success' passes):" + ((${#rows[@]} > 0)) || { echo "::error title=CI gate::no required jobs found"; exit 1; } offenders=0 for row in "${rows[@]}"; do job="${row%%$'\t'*}" result="${row#*$'\t'}" - if [ "$result" = "success" ]; then - printf ' ✅ %-12s %s\n' "$job" "$result" + if [ "$result" = success ]; then + printf ' OK %-20s %s\n' "$job" "$result" else - printf ' ❌ %-12s %s\n' "$job" "$result" - echo "::error title=CI gate::required job '${job}' concluded '${result}', not 'success'." + printf ' FAIL %-20s %s\n' "$job" "$result" + echo "::error title=CI gate::required job '${job}' concluded '${result}', not 'success'" offenders=$((offenders + 1)) fi done - if [ "$offenders" -ne 0 ]; then - echo "❌ CI failed: ${offenders} of ${#rows[@]} required job(s) did not conclude 'success' (see the ❌ rows above)." + echo "CI failed: ${offenders} of ${#rows[@]} required job(s) did not conclude 'success'." exit 1 fi - echo "✅ CI passed: all ${#rows[@]} required job(s) concluded 'success'." + echo "CI passed: all ${#rows[@]} required job(s) concluded 'success'." diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index 0f358a9d..00000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,381 +0,0 @@ -name: Release - -on: - push: - branches: - - main - # Manual re-trigger for re-attaching binaries to an ALREADY-released tag - # (e.g. when the original build-binaries jobs failed). - workflow_dispatch: - inputs: - tag_name: - description: "Existing release tag to (re)build binaries for (e.g. v0.1.0)" - required: true - type: string - version: - description: "Version string for artifact names (e.g. 0.1.0)" - required: true - type: string - -permissions: - contents: write - pull-requests: write - # verify-ci's `gh api` calls read the CI workflow's runs + jobs. - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -env: - CARGO_TERM_COLOR: always - BINARY_NAME: codegraph - -jobs: - # ---------------------------------------------------------------- - # release-please: open/maintain the release PR; on merge, cut a tag - # and emit outputs that gate the heavy jobs below. - # - # The GitHub Release is created as a DRAFT (draft: true in - # release-please-config.json) with the git tag forced into existence - # immediately (force-tag-creation: true) so the build/upload jobs can - # check out the tag. The release is flipped to published (draft=false) - # only by the final publish-release job, after all builds + asset - # uploads succeed — closing the old zero/partial-asset public window. - # ---------------------------------------------------------------- - release-please: - name: Release Please - runs-on: ubuntu-latest - outputs: - build_artifacts: ${{ steps.gate.outputs.build_artifacts }} - tag_name: ${{ steps.gate.outputs.tag_name }} - version: ${{ steps.gate.outputs.version }} - release_created: ${{ steps.release.outputs.release_created }} - steps: - - name: Run release-please - id: release - if: github.event_name == 'push' - uses: googleapis/release-please-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - config-file: release-please-config.json - manifest-file: .release-please-manifest.json - - - name: Resolve build gate - id: gate - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "build_artifacts=true" >> "$GITHUB_OUTPUT" - echo "tag_name=${{ inputs.tag_name }}" >> "$GITHUB_OUTPUT" - echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - echo "build_artifacts=${{ steps.release.outputs.release_created }}" >> "$GITHUB_OUTPUT" - echo "tag_name=${{ steps.release.outputs.tag_name }}" >> "$GITHUB_OUTPUT" - echo "version=${{ steps.release.outputs.version }}" >> "$GITHUB_OUTPUT" - fi - - # ---------------------------------------------------------------- - # verify-ci: gate the release on the SAME tag commit's CI result. - # - # We do NOT re-run cargo tests here — we QUERY the existing `CI` workflow - # run for the exact tag commit SHA (event=push, branch=main) and wait for - # its `CI Success` job to conclude. Gating on that single job (which itself - # needs test/audit/windows) is the right wait point: it excludes the slow - # informational Coverage job. - # - # Topology: this runs IN PARALLEL with build-binaries (build-binaries needs - # only release-please), so the ~430s cross-compile overlaps with the CI wait. - # The gate is enforced downstream at upload-assets (which lists verify-ci in - # `needs`) — the first public-facing step that touches the GitHub Release. - # build-binaries only produces throwaway artifacts (retention-days: 1), so a - # failed/timed-out verify-ci blocks upload-assets + publish-release and leaves - # the Release a DRAFT, without weakening the gate. - # - # This job ALWAYS RUNS when a tag was cut (build_artifacts=='true'); it must - # never be SKIPPED, because upload-assets lists it in `needs` and a skipped - # needed job would cascade-skip upload-assets. On the workflow_dispatch - # (manual re-attach) path there is no new CI run, so the job short-circuits - # to success as a no-op; the polling steps run only on the push path. - # ---------------------------------------------------------------- - verify-ci: - name: Verify CI Passed - needs: release-please - if: needs.release-please.outputs.build_artifacts == 'true' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Manual re-attach — skip CI gate (no-op success) - if: github.event_name == 'workflow_dispatch' - run: | - echo "workflow_dispatch: re-attaching binaries to an existing tag; no new CI run to gate on — passing." - - - name: Checkout tag - if: github.event_name == 'push' - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.release-please.outputs.tag_name }} - - - name: Resolve tag commit SHA - id: sha - if: github.event_name == 'push' - run: echo "sha=$(git rev-list -n1 '${{ needs.release-please.outputs.tag_name }}')" >> "$GITHUB_OUTPUT" - - - name: Wait for CI Success on the tag commit - if: github.event_name == 'push' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - HEAD_SHA: ${{ steps.sha.outputs.sha }} - run: | - set -euo pipefail - # Bounded poll: ~25 min cap (100 iterations x 15s) inside timeout-minutes: 30. - max_iters=100 - sleep_secs=15 - run_id="" - for i in $(seq 1 "$max_iters"); do - # Select the CI workflow's push run on main for the exact tag SHA; - # if several exist, take the most recent by created_at. - run_id=$(gh api "repos/${REPO}/actions/runs?head_sha=${HEAD_SHA}&event=push&branch=main&per_page=100" \ - --jq '[.workflow_runs[] | select(.name == "CI")] | sort_by(.created_at) | last | .id // empty') - if [ -n "$run_id" ]; then - echo "Found CI run ${run_id} for ${HEAD_SHA}" - break - fi - echo "No CI run for ${HEAD_SHA} yet (attempt ${i}/${max_iters}); sleeping ${sleep_secs}s..." - sleep "$sleep_secs" - done - if [ -z "$run_id" ]; then - echo "❌ Timed out waiting for a CI run on ${HEAD_SHA}; leaving release as draft." - exit 1 - fi - for i in $(seq 1 "$max_iters"); do - status=$(gh api "repos/${REPO}/actions/runs/${run_id}/jobs?per_page=100" \ - --jq '.jobs[] | select(.name == "CI Success") | .status') - conclusion=$(gh api "repos/${REPO}/actions/runs/${run_id}/jobs?per_page=100" \ - --jq '.jobs[] | select(.name == "CI Success") | .conclusion // ""') - if [ -z "$status" ]; then - echo "CI Success job not present in run ${run_id} yet (attempt ${i}/${max_iters}); sleeping ${sleep_secs}s..." - sleep "$sleep_secs" - continue - fi - if [ "$status" != "completed" ]; then - echo "CI Success job status=${status} (attempt ${i}/${max_iters}); sleeping ${sleep_secs}s..." - sleep "$sleep_secs" - continue - fi - if [ "$conclusion" = "success" ]; then - echo "✅ CI Success passed for ${HEAD_SHA}." - exit 0 - fi - echo "❌ CI Success concluded '${conclusion}' for ${HEAD_SHA}; blocking release (draft preserved)." - exit 1 - done - echo "❌ Timed out waiting for CI Success to complete on ${HEAD_SHA}; leaving release as draft." - exit 1 - - # ---------------------------------------------------------------- - # build-binaries: cross-compile release artifacts for all targets. - # Gated on a cut tag. The CLI crate is `codegraph-rs`, binary `codegraph`. - # ---------------------------------------------------------------- - build-binaries: - name: Build ${{ matrix.target }} - needs: release-please - if: needs.release-please.outputs.build_artifacts == 'true' - runs-on: ${{ matrix.os }} - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - use_zigbuild: true - archive: tar.gz - - target: aarch64-unknown-linux-musl - os: ubuntu-latest - use_zigbuild: true - archive: tar.gz - - target: x86_64-apple-darwin - os: macos-latest - use_zigbuild: false - archive: tar.gz - - target: aarch64-apple-darwin - os: macos-latest - use_zigbuild: false - archive: tar.gz - - target: x86_64-pc-windows-msvc - os: windows-latest - use_zigbuild: false - archive: zip - - target: aarch64-pc-windows-msvc - os: windows-11-arm - use_zigbuild: false - archive: zip - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - ref: ${{ needs.release-please.outputs.tag_name }} - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 - with: - key: ${{ matrix.target }} - - - name: Install Zig - if: matrix.use_zigbuild - uses: mlugg/setup-zig@v2 - with: - version: 0.13.0 - - - name: Install cargo-zigbuild - if: matrix.use_zigbuild - uses: taiki-e/install-action@v2 - with: - tool: cargo-zigbuild - - - name: Add Rust target - run: rustup target add ${{ matrix.target }} - - - name: Build (zigbuild) - if: matrix.use_zigbuild - run: cargo zigbuild --release -p codegraph-rs --target ${{ matrix.target }} - - - name: Build (native) - if: ${{ !matrix.use_zigbuild }} - run: cargo build --release -p codegraph-rs --target ${{ matrix.target }} - - - name: Package (unix tar.gz) - if: matrix.archive == 'tar.gz' - shell: bash - run: | - mkdir -p dist - tar -czf "dist/${BINARY_NAME}-${{ needs.release-please.outputs.version }}-${{ matrix.target }}.tar.gz" \ - -C "target/${{ matrix.target }}/release" "${BINARY_NAME}" - - - name: Package (windows zip) - if: matrix.archive == 'zip' - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path dist | Out-Null - Compress-Archive ` - -Path "target/${{ matrix.target }}/release/${env:BINARY_NAME}.exe" ` - -DestinationPath "dist/${env:BINARY_NAME}-${{ needs.release-please.outputs.version }}-${{ matrix.target }}.zip" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: dist-${{ matrix.target }} - path: dist/* - retention-days: 1 - - # ---------------------------------------------------------------- - # upload-assets: gather all artifacts, render release notes with - # git-cliff, and attach binaries to the (still-DRAFT) GitHub Release. - # release-please created the Release as a draft (release-please-config.json - # "draft": true), so the softprops step keeps draft:true here — it only - # updates the draft with notes + assets, it must NOT publish. - # ---------------------------------------------------------------- - upload-assets: - name: Upload Release Assets - needs: [release-please, build-binaries, verify-ci] - if: needs.release-please.outputs.build_artifacts == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.release-please.outputs.tag_name }} - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - pattern: dist-* - path: dist - merge-multiple: true - - # Checksums are computed HERE, from the very same merged `dist/` that the - # attach step below uploads, so the sums provably describe the uploaded - # bytes. Determinism: `LC_ALL=C sort` on basenames gives a byte-order, - # locale-independent ordering; sha256sum is run with cwd=dist so each line - # carries the bare asset name (no `dist/` prefix); output is LF-only. - - name: Generate SHA256SUMS - shell: bash - working-directory: dist - run: | - set -euo pipefail - shopt -s nullglob - archives=(*.tar.gz *.zip) - if [ "${#archives[@]}" -eq 0 ]; then - echo "::error::no release archives found in dist/" >&2 - exit 1 - fi - mapfile -t sorted < <(printf '%s\n' "${archives[@]}" | LC_ALL=C sort) - : > SHA256SUMS - for archive in "${sorted[@]}"; do - sha256sum "$archive" >> SHA256SUMS - done - echo "--- SHA256SUMS ---" - cat SHA256SUMS - - - name: Generate release notes - uses: orhun/git-cliff-action@v4 - with: - config: cliff.toml - args: --latest --strip header - env: - OUTPUT: RELEASE_NOTES.md - - - name: Attach assets to GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.release-please.outputs.tag_name }} - body_path: RELEASE_NOTES.md - # Keep the release a DRAFT while attaching assets; the dedicated - # publish-release job below flips it to published only on full success. - draft: true - files: | - dist/*.tar.gz - dist/*.zip - dist/SHA256SUMS - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # ---------------------------------------------------------------- - # publish-release: flip the draft Release to PUBLISHED (draft=false) and - # mark it "latest" — but ONLY after every build-binaries matrix job AND - # upload-assets succeeded. - # - # WHY (draft-until-all-green): release-please creates the Release as a DRAFT - # (config "draft": true) and the tag immediately (config "force-tag-creation": - # true). Without this gate the Release would be PUBLIC the instant release-please - # ran — i.e. before any binary was built — leaving a window where a public - # "latest" Release has zero/partial assets if a build later fails. By deferring - # publication to this terminal job, a failed build leaves a *visible draft* - # (observable failure) instead of a broken public release. The default `needs` - # semantics require all listed jobs to succeed, so this job only runs on a fully - # green build+upload; otherwise the Release stays a draft. - # ---------------------------------------------------------------- - publish-release: - name: Publish Release - needs: [release-please, build-binaries, upload-assets] - if: needs.release-please.outputs.build_artifacts == 'true' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Publish the draft Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release edit "${{ needs.release-please.outputs.tag_name }}" \ - --repo "${{ github.repository }}" \ - --draft=false \ - --latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..0e85c78e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,557 @@ +# Same-run Rust release: release-please owns version, tag, and a draft Release; +# verify and build legs run downstream in this run; the archives are attested in +# the no-compile upload-assets job; the terminal job verifies every remote asset +# and flips the draft. Requires .github/scripts/resolve-release-gate.sh and +# .github/scripts/publish-release-draft.sh from the shared asset set. +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + tag_name: + description: "Existing draft release tag to rebuild, for example v1.2.3" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + BINARY_NAME: codegraph + CRATE_NAME: codegraph-rs + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: sccache + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + outputs: + build_artifacts: ${{ steps.gate.outputs.build_artifacts }} + prerelease: ${{ steps.gate.outputs.prerelease }} + release_sha: ${{ steps.gate.outputs.release_sha }} + tag_name: ${{ steps.gate.outputs.tag_name }} + version: ${{ steps.gate.outputs.version }} + steps: + - name: Run release-please + id: release + if: github.event_name == 'push' + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Resolve release gate + id: gate + shell: bash + env: + DEFAULT_BRANCH: main + EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} + INPUT_TAG: ${{ inputs.tag_name }} + REF_PROTECTED: ${{ github.ref_protected }} + RELEASE_CREATED: ${{ steps.release.outputs.release_created }} + RELEASE_SHA: ${{ steps.release.outputs.sha }} + RELEASE_TAG: ${{ steps.release.outputs.tag_name }} + RELEASE_VERSION: ${{ steps.release.outputs.version }} + REPO: ${{ github.repository }} + RUN_REF: ${{ github.ref }} + run: bash .github/scripts/resolve-release-gate.sh + + source-gate: + name: Release Source Gate + needs: release-please + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + + - name: Require the tag to resolve to the release commit + shell: bash + env: + RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} + run: | + set -euo pipefail + if [ "$(git rev-parse HEAD)" != "$RELEASE_SHA" ]; then + echo "::error title=Release source::checked-out tag differs from the release commit" + exit 1 + fi + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 + + - name: Require the version gate's parser + run: command -v jq >/dev/null 2>&1 || { echo "jq is required"; exit 1; } + + # The first Cargo subprocess in a release is the non-mutating version gate. + - name: Verify workspace version surfaces + run: bash scripts/check-workspace-versions.sh + + verify-ci: + name: Verify CI Passed + needs: release-please + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 35 + permissions: + actions: read + contents: read + steps: + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + fetch-depth: 0 + persist-credentials: false + + - name: Require the tag to resolve to the release commit + shell: bash + env: + RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} + run: | + set -euo pipefail + if [ "$(git rev-parse HEAD)" != "$RELEASE_SHA" ]; then + echo "::error title=Release CI::checked-out tag differs from the release commit" + exit 1 + fi + + - name: Wait for exact-SHA CI Success + shell: bash + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ needs.release-please.outputs.release_sha }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + run_id="" + for _ in $(seq 1 120); do + run_id=$(gh api "repos/${REPO}/actions/runs?head_sha=${HEAD_SHA}&event=push&branch=main&per_page=100" \ + --jq '[.workflow_runs[] | select(.name == "CI")] | sort_by(.created_at) | last | .id // empty') + [ -z "$run_id" ] || break + sleep 15 + done + if [ -z "$run_id" ]; then + echo "::error title=Release CI::no CI push run found for ${HEAD_SHA}" + exit 1 + fi + + for _ in $(seq 1 120); do + job=$(gh api "repos/${REPO}/actions/runs/${run_id}/jobs?per_page=100" \ + --jq '[.jobs[] | select(.name == "CI Success")] | last // {}') + status=$(jq -r '.status // empty' <<<"$job") + conclusion=$(jq -r '.conclusion // empty' <<<"$job") + if [ "$status" != completed ]; then + sleep 15 + continue + fi + if [ "$conclusion" = success ]; then + echo "CI Success passed for exact release SHA ${HEAD_SHA} (run ${run_id})." + exit 0 + fi + echo "::error title=Release CI::CI Success concluded ${conclusion:-} for ${HEAD_SHA}" + exit 1 + done + echo "::error title=Release CI::timed out waiting for CI Success in run ${run_id}" + exit 1 + + verify: + name: Verify release tag + needs: [release-please, source-gate] + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + + - name: Require the tag to resolve to the release commit + shell: bash + env: + RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} + run: | + set -euo pipefail + if [ "$(git rev-parse HEAD)" != "$RELEASE_SHA" ]; then + echo "::error title=Release verify::checked-out tag differs from the release commit" + exit 1 + fi + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 + components: rustfmt,clippy + + - name: Verify workspace version surfaces before Cargo/cache + run: bash scripts/check-workspace-versions.sh + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + cache-targets: false + + - name: Configure compiler cache + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: "v0.16.0" + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + + - name: Install verification tools + run: npm install --global oxfmt@0.64.0 + + - name: Install pinned actionlint + run: bash .github/scripts/install-actionlint.sh + + - name: Require fixture parsers and shell linter + run: | + set -euo pipefail + command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck is required"; exit 1; } + python3 -c 'import yaml' \ + || python3 -m pip install --break-system-packages PyYAML==6.0.3 \ + || python3 -m pip install PyYAML==6.0.3 + + - name: Check + run: make check + + - name: Install cargo-audit + uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4 + with: + tool: cargo-audit@0.22.2 + + - name: Audit dependencies + run: cargo audit + + build-binaries: + name: Build ${{ matrix.target }} + needs: [release-please, source-gate] + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + # Build legs execute build scripts and dependency code, so they hold no + # id-token or attestations permission; upload-assets attests the archives. + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + runner: ubuntu-24.04 + platform: linux + use_zigbuild: true + archive: tar.gz + - target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm + platform: linux + use_zigbuild: true + archive: tar.gz + - target: x86_64-apple-darwin + runner: macos-15 + platform: macos + execution_arch: x86_64 + use_zigbuild: false + archive: tar.gz + - target: aarch64-apple-darwin + runner: macos-15 + platform: macos + execution_arch: arm64 + use_zigbuild: false + archive: tar.gz + - target: x86_64-pc-windows-msvc + runner: windows-2025 + platform: windows + use_zigbuild: false + archive: zip + - target: aarch64-pc-windows-msvc + runner: windows-11-arm + platform: windows + use_zigbuild: false + archive: zip + steps: + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + + - name: Require the tag to resolve to the release commit + shell: bash + env: + RELEASE_SHA: ${{ needs.release-please.outputs.release_sha }} + run: | + set -euo pipefail + if [ "$(git rev-parse HEAD)" != "$RELEASE_SHA" ]; then + echo "::error title=Release build::checked-out tag differs from the release commit" + exit 1 + fi + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-08-21 + with: + toolchain: 1.96.0 + targets: ${{ matrix.target }} + + - name: Verify workspace version surfaces before Cargo/cache + shell: bash + run: bash scripts/check-workspace-versions.sh + + - name: Cache Rust artifacts + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: cargo-home-${{ runner.os }}-${{ runner.arch }} + key: ${{ matrix.target }} + cache-targets: false + cache-workspace-crates: false + + - name: Configure compiler cache + uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 + with: + version: "v0.16.0" + + - name: Install Zig + if: matrix.use_zigbuild + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.15.1 + + - name: Install cargo-zigbuild + if: matrix.use_zigbuild + uses: taiki-e/install-action@a2a5f6e99e1a31540baa0468acfa302cff0f359f # v2.86.4 + with: + tool: cargo-zigbuild@0.23.0 + + - name: Populate the Cargo registry + run: cargo fetch --locked + + - name: Compute the archive name once + id: names + shell: bash + env: + ARCHIVE_KIND: ${{ matrix.archive }} + TARGET: ${{ matrix.target }} + VERSION: ${{ needs.release-please.outputs.version }} + run: | + set -euo pipefail + echo "archive=${BINARY_NAME}-${VERSION}-${TARGET}.${ARCHIVE_KIND}" >>"$GITHUB_OUTPUT" + + - name: Build with cargo-zigbuild + if: matrix.use_zigbuild + shell: bash + env: + TARGET: ${{ matrix.target }} + run: cargo zigbuild --locked --release --timings -p "$CRATE_NAME" --target "$TARGET" + + - name: Build natively + if: ${{ !matrix.use_zigbuild }} + shell: bash + env: + TARGET: ${{ matrix.target }} + run: cargo build --locked --release --timings -p "$CRATE_NAME" --target "$TARGET" + + - name: Package and unpack tar.gz + if: matrix.archive == 'tar.gz' + shell: bash + env: + ARCHIVE: ${{ steps.names.outputs.archive }} + TARGET: ${{ matrix.target }} + run: | + set -euo pipefail + mkdir -p dist unpacked + tar -czf "dist/${ARCHIVE}" -C "target/${TARGET}/release" "$BINARY_NAME" + tar -xzf "dist/${ARCHIVE}" -C unpacked + chmod +x "unpacked/$BINARY_NAME" + + - name: Package and unpack zip + if: matrix.archive == 'zip' + shell: pwsh + env: + ARCHIVE: ${{ steps.names.outputs.archive }} + TARGET: ${{ matrix.target }} + run: | + New-Item -ItemType Directory -Force -Path dist,unpacked | Out-Null + Compress-Archive ` + -Path "target/${env:TARGET}/release/${env:BINARY_NAME}.exe" ` + -DestinationPath "dist/${env:ARCHIVE}" + Expand-Archive -Path "dist/${env:ARCHIVE}" -DestinationPath unpacked -Force + + - name: Smoke packaged artifact on Linux + if: matrix.platform == 'linux' + shell: bash + run: '"unpacked/$BINARY_NAME" --version' + + - name: Smoke packaged artifact on macOS + if: matrix.platform == 'macos' + shell: bash + env: + EXECUTION_ARCH: ${{ matrix.execution_arch }} + run: | + set -euo pipefail + /usr/bin/lipo "unpacked/$BINARY_NAME" -verify_arch "$EXECUTION_ARCH" + /usr/bin/arch "-${EXECUTION_ARCH}" "unpacked/$BINARY_NAME" --version + + - name: Smoke packaged artifact on Windows + if: matrix.platform == 'windows' + shell: pwsh + run: '& "unpacked/${env:BINARY_NAME}.exe" --version' + + - name: Upload smoked build artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist-${{ matrix.target }} + path: dist/* + if-no-files-found: error + retention-days: 1 + + - name: Collect compiler-cache statistics + if: always() + shell: bash + run: | + mkdir -p diagnostics + sccache --show-stats --stats-format=json >diagnostics/sccache-stats.json || true + + - name: Upload build diagnostics + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: diagnostics-${{ matrix.target }} + path: | + target/cargo-timings/*.html + diagnostics/ + if-no-files-found: warn + retention-days: 3 + + upload-assets: + name: Attach release assets + needs: [release-please, source-gate, verify-ci, verify, build-binaries] + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + # Downloads, hashes, attests, and uploads; executes no project code, so it + # is the only job holding id-token: write. + permissions: + attestations: write + contents: write + id-token: write + steps: + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Download build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: dist-* + path: dist + merge-multiple: true + + - name: Generate SHA256SUMS + shell: bash + working-directory: dist + run: | + set -euo pipefail + shopt -s nullglob + archives=(*.tar.gz *.zip) + ((${#archives[@]} > 0)) || { echo "::error::no release archives found"; exit 1; } + printf '%s\n' "${archives[@]}" | LC_ALL=C sort | xargs sha256sum >SHA256SUMS + + - name: Attest release archives + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: dist/SHA256SUMS + + - name: Attach assets to draft + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.release-please.outputs.tag_name }} + run: | + set -euo pipefail + mapfile -t files < <(awk '{ print "dist/" $2 }' dist/SHA256SUMS) + gh release upload "$TAG" "${files[@]}" dist/SHA256SUMS \ + --repo "$GITHUB_REPOSITORY" \ + --clobber + + - name: Verify remote asset bytes + shell: bash + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.release-please.outputs.tag_name }} + run: | + bash .github/scripts/publish-release-draft.sh --verify-only \ + --checksums dist/SHA256SUMS --asset dist/SHA256SUMS --exact + + - name: Upload checksums for the publish job + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: checksums + path: dist/SHA256SUMS + if-no-files-found: error + retention-days: 1 + + publish-release: + name: Publish Release + needs: [release-please, upload-assets] + if: needs.release-please.outputs.build_artifacts == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + environment: release + permissions: + contents: write + steps: + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Download checksums + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: checksums + path: dist + + - name: Verify remote assets and publish the draft + shell: bash + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.release-please.outputs.tag_name }} + run: | + bash .github/scripts/publish-release-draft.sh --publish \ + --checksums dist/SHA256SUMS --asset dist/SHA256SUMS --exact diff --git a/.oxfmtignore b/.oxfmtignore index 2abf44c7..c7a9319f 100644 --- a/.oxfmtignore +++ b/.oxfmtignore @@ -1,10 +1,7 @@ # oxfmt ignore — files oxfmt must NOT touch. # -# Auto-generated changelog / release-please manifests are produced and formatted -# by the release workflow (update-changelog job); the dev/CI fmt flow must not -# touch them, otherwise git-cliff regeneration conflicts with oxfmt's expected -# format and fmt-check would falsely fail on every PR between releases. -CHANGELOG.md +# Release-please owns changelog and manifest bytes; the general repository +# formatter must not rewrite them between release PR updates. changelog/ .release-please-manifest.json release-please-config.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..34e1c419 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,8 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSameLine": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/AGENTS.md b/AGENTS.md index 2e762202..eedd0dc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,398 +1,188 @@ -# AGENTS.md — codegraph-rs +# AGENTS.md — codegraph-rs contributor contract -A deterministic tree-sitter + SQLite/FTS5 **code knowledge graph**: it parses a codebase, -extracts symbols and their relationships, persists them to a per-project SQLite database -(with an FTS5 search index), and exposes the result through a CLI and an MCP (Model Context -Protocol) stdio server. No AI / vector / LLM anywhere in the binary — output is byte-stable. +CodeGraph is a deterministic tree-sitter + SQLite/FTS5 code knowledge graph. The +single `codegraph` binary indexes source, resolves relationships, answers graph +queries through the CLI and MCP, and can keep an index current through a local +daemon. It contains no AI, vector database, embedding model, or LLM runtime. -## CodeGraph command contract +This file is the canonical instruction source for coding agents. `CLAUDE.md` +imports it; do not create a second agent guide. Detailed, changeable behavior +belongs in [`docs/`](docs/README.md), not here. -Before code research, run `codegraph status . --json`. Lifecycle commands -(`status`, `init`, `sync`, `index`) accept an optional positional project path. -Research commands take one positional query/target and select the project with -`-p/--path`: +## Navigate with CodeGraph first + +Before researching source in an indexed checkout, verify the index: + +```bash +codegraph status . --json +``` + +Lifecycle commands take an optional positional project path. Research commands +take one query/target plus `-p/--path`: ```bash -codegraph explore "" -p . -codegraph search "" -p . -codegraph node "" -p . +codegraph status . --json +codegraph sync . +codegraph explore "index and resolve flow" -p . +codegraph search "ReferenceResolver" -p . +codegraph node "ReferenceResolver" -p . ``` -Do not append `.` as a second positional argument to a research command. If an -argument is rejected, run `codegraph --help` and correct the syntax -before falling back to grep. `node` accepts a symbol name, qualified name, exact -`node.id` from `search --json`, or an indexed file path. - -## Hard invariants (never break) - -- **Golden `.schema` byte-stability** — verified by `crates/codegraph-bench/tests/equivalence.rs` - against the fixed golden artifacts under `reference/golden/`. Fixtures: the existing upstream - corpus plus `reference/golden/godot/` (corpus `crates/codegraph-bench/fixtures/godot/`; - guards F1 autoload-call edges + F2 signal-handler edges + UID-form autoloads - — a sidecar-UID script autoload (`*.gd.uid`) and a header-UID scene autoload - (`.tscn` `uid=`) — byte-for-byte) and - `reference/golden/ruby/` (corpus `crates/codegraph-bench/fixtures/ruby/`; guards #1110 - Ruby `receiver.method` extraction — instance/class-method Calls, `Const.new` Instantiates, - bare `include` Implements — byte-for-byte), `reference/golden/python/` (corpus - `crates/codegraph-bench/fixtures/python/`; guards six bare class-as-value - `References` edges — return, assignment RHS, registry pair, call argument, - list literal, and one cross-file unique match — plus tuple-return and bare-method - negatives, plus #1626 Python module/member import aliases and their no-global-fallback - boundaries, byte-for-byte), `reference/golden/kotlin/` (corpus - `crates/codegraph-bench/fixtures/kotlin/`; guards #1495 Kotlin callable - signatures for explicit, inferred, generic, multiline nullable, and extension - returns, plus a primary-constructor negative — byte-for-byte), - `reference/golden/typescript/` (corpus `crates/codegraph-bench/fixtures/typescript/`; - guards #1482 local export aliases and #1482b TypeScript `.js`-specifier - substitution, object-literal callable members, inherited/config-relative TS - path aliases, and `.xsjs`/`.xsjslib` import candidates with ordinary-JS - precedence — byte-for-byte), and - `reference/golden/cpp/` - (corpus `crates/codegraph-bench/fixtures/cpp/`; guards #1043 C++ class/struct - inheritance incl. templated-base stripping byte-for-byte, and retroactively - the earlier C++ extraction work; grown to 19 files with full-source lexically - masked plain-derived `.h` detection and its text/bitfield/label/ternary - negatives, plus the Tier 3 MSVC COM - `interface` positives + five guard negatives (#1519) and the C / C++ / ObjC - `union` fixtures incl. both `typedef union` re-kinds and both instantiation - paths (PR #1516) — it is the C-FAMILY corpus, not a strictly-C++ one, since it - holds `.c` and `.mm` files too), and `reference/golden/rust/` - (four-file corpus `crates/codegraph-bench/fixtures/rust/`; the repo's only Rust corpus, - guards #1513/PR #1514 unit structs — 3 `struct` nodes and 4 `implements` edges, - where the unit struct produced NO node before — plus the Rust half of PR #1516's - first-class `union` kind, generic/lifetime/reference/qualified impl ownership, - and the statically safe subset of `self.field.method()` resolution, byte-for-byte; - it deliberately claims NO `instantiates` edge, because that edge fires only for - the call-expression construction form and a Rust union is constructible only as - `U { … }`), `reference/golden/lua/` (single-file corpus - `crates/codegraph-bench/fixtures/lua/`; guards #1616 local/table-field function - expressions, nested qualified names, body-call ownership, dot/colon calls, and - dynamic-key negatives — byte-for-byte), `reference/golden/erlang/` (seven-file - corpus `crates/codegraph-bench/fixtures/erlang/`; guards bare display names plus - `module::function/arity` identity across clauses, exports, specs, local/remote - calls, fun values, safe MFA dispatch, and binary-literal comma counting — - byte-for-byte), and `reference/golden/go/` - (corpus `crates/codegraph-bench/fixtures/go/`; the repo's only Go corpus, and the - only corpus where `files.generated` is anything other than `0` — it byte-pins - #1500 content-header detection at BOTH values, three each way: `payroll.go` - (Go's codified `Code generated … DO NOT EDIT.`), `worker_types.go` (pattern 6 / - CG-25, Wrangler's TWO `by` clauses) and `api.pb.go` (the PATH signal, no banner) - at `1`; `payroll_usecase.go` (hand-written, and the must-not-demote side), - `nightly.go` (ONE `by` clause — ordinary prose) and `generator.go` (the banner is - a `const` in the function BODY, so the comment-line fence rejects it) at `0`). - The Python cross-file edge is resolved by Gate 3a's unique-name fallback; - import Gate 3b is deliberately unreachable for real Python nodes because the - extractor does not mark them exported. Regen recipe: `docs/equivalence.md` - "Godot fixture" / "Ruby fixture" / "Python fixture" / "Kotlin fixture" / - "TypeScript resolution fixture" / "C++ fixture" / "Rust fixture" / "Lua fixture" / - "Erlang fixture" / "Go fixture" sections. `mini` has no re-indexable provenance - and a schema migration cannot normalise it in place, so it has its own - "Mini schema rebuild" recipe there. -- **node-id formula**: `{kind}:{sha256("{filePath}:{kind}:{name}:{line}").hex[:32]}`; file nodes are the - literal `file:{relpath}`; lines are 1-based; paths relative with `/`. -- **No AI / vector / LLM crates** — enforced by `scripts/guardrail.sh` (CI gate): - no surrealdb / rig / qdrant / lancedb / candle / onnx / ort. -- **Deterministic** extraction + resolution; sync output must equal `index --force` byte-for-byte. - -## Workspace layout (10 crates) - -`codegraph-core` (types/config/logger) · `codegraph-store` (SQLite+FTS5) · `codegraph-extract` -(tree-sitter walker + embedded + custom extractors; incl. C++ `base_class_clause` → `Extends` -inheritance extraction with templated-base stripping, #1043; a shared `extract_aggregate` behind -`extract_struct`/`extract_union` so the `union` kind — C/C++/ObjC `union_specifier`, Rust -`union_item`, PR #1516 — inherits struct behaviour by construction, plus the opt-in -`allow_bodiless_struct()` that makes a Rust `struct Unit;` a definition rather than a forward -declaration, #1513) · `codegraph-graph` (traversal + FTS search) · -`codegraph-resolve` (import + name matcher + FrameworkResolver; concrete `GodotResolver` impl — autoload-call + signal-handler resolution) · `codegraph-mcp` -(stdio JSON-RPC; `codegraph_explore` runs a change-surface rescue, #1064, that surfaces a -callable's buried parameter/return-type files into the explored subgraph; every source-emitting -path goes through a request-memoized freshness probe — size + ms-mtime, then sha256 on stat -mismatch — so a drifted file is served whole under `FILE_MODE_MAX_LINES` or omitted, never sliced -at stored line numbers, and the response carries the staleness banner only when it actually cites -one) · `codegraph-cli` (single binary, owns logger; also hosts the `install`/`uninstall` -agent-config installer in `src/installer/`) · `codegraph-daemon` · -`codegraph-watch` · `codegraph-bench` (benchmark harness + golden oracle). - -The published crate is `codegraph-rs` (the `codegraph-cli` package); the installed binary is -`codegraph`. The library crates publish as `codegraph-{core,store,extract,graph,resolve,mcp,daemon,watch}`. -`codegraph-bench` is `publish = false`. - -## Godot framework resolver (`codegraph-resolve`) - -The `GodotResolver` is the first concrete `FrameworkResolver` impl. It fires on -GDScript files and synthesizes edges that tree-sitter alone cannot produce. Three -behaviors are active: - -- **F1 — autoload-call→func edges**: a call `Autoload.method()` in a `.gd` file - emits a `Calls` edge to the UNIQUE same-named `func` in the autoload's bound - target script (binding read from `project.godot` `[autoload]` section, - `Name="*res://path.gd"` form only). Determinism rule: edge built ONLY when - exactly one matching `func` exists in that script; 0 or ≥2 matches → no edge. - Files: `crates/codegraph-resolve/src/frameworks/godot.rs`, - `crates/codegraph-resolve/src/frameworks/godot_script.rs`. - -- **F2 — signal handler extraction**: `connect_handler` now extracts handlers - from `.connect(_h.bind(x))` (head segment before `.bind(`) and - `Callable(self,"h")`/`Callable(this,"h")` forms, in addition to bare - `.connect(_h)`. Other receivers, variable handlers, or non-literal method - names stay dynamic sentinels (unresolved). File: - `crates/codegraph-resolve/src/frameworks/godot_script.rs`. - -- **F3 — impact/affected↔audit unification**: `codegraph impact` (file-node - targets) and `codegraph affected` now also consume path-keyed `unresolved_refs` - restricted to Godot `ReferenceSubkind`s (`script_attach`, `ext_resource`, - `scene_instance`, `group_member`, `signal_method`, `autoload`), so their - output agrees with `codegraph audit --impact`. Query-side only; zero extraction - change. New function: `dependent_file_paths_unresolved` in - `crates/codegraph-store/src/queries.rs`; CLI wired in - `crates/codegraph-cli/src/main.rs`. `codegraph affected` additionally emits an - `affectedFiles` key — the sorted+deduped union of every traversed dependent - plus the test-file set (`affectedFiles ⊇ affectedTests`) — so it LISTS the - complete affected set instead of only counting it via - `totalDependentsTraversed`. Additive: `changedFiles`, `affectedTests`, - `totalDependentsTraversed` are byte-for-byte unchanged. - -Full Godot static-analysis scope, static-vs-runtime boundary, and honesty signals: -[`docs/godot.md`](docs/godot.md). - -## Tauri IPC resolver (`codegraph-resolve`) - -The `TauriResolver` is the second concrete `FrameworkResolver` impl. It bridges the -IPC boundary a `#[tauri::command]` opens: `extract()` emits one -`tauri:invoke:` `Calls` reference per literal-argument `invoke('name')` -call site in a JS-family file, and `resolve()` binds it to the command function. -Registered **LAST** in `detect_frameworks` so the existing four resolvers' Strategy-1 -iteration order is unchanged. `confidence` is 0.9 at its single return site, so it -short-circuits before the name matcher. File: -`crates/codegraph-resolve/src/frameworks/tauri.rs`. - -- **Determinism rule.** An edge is built only when EXACTLY ONE command claims the - wire name. The roster is a `BTreeMap>` keyed by both the - snake name and its `tauri-specta` camelCase spelling, and a key claimed by a - second, DIFFERENT target is permanently poisoned (`None`) — 0 or ≥2 candidates - produce no edge. The "different target id" clause is what keeps a single-word - command, whose camel spelling equals its wire name, from poisoning itself. This - rule is the ONLY protection that exists: `Language::Rust` has no language family, - so the cross-family gates are inert for a Rust target, and `Calls` bypasses them - regardless. -- **Both text scans are lexically masked, and dropping either one is a defect.** - `rust_code_mask` gates the roster — BOTH the `#[tauri::command]` token and the - `fn ` that follows it, skipping masked bytes and further attributes in - between (a whole-span gate fails closed on the legal attribute / doc-comment / - attribute / `fn` interleaving) — plus a token boundary after `command`, because - `#[tauri::commandant]` is entirely ordinary code and the mask cannot reject it. - Without those, a commented-out or raw-string copy of the attribute promotes the - real UNATTRIBUTED same-named function: measured on the adversarial fixture, six - fabricated edges where the correct answer is one, since every masked copy mints - no node and the same-file/same-name join therefore has exactly one candidate and - it is the wrong one. `js_code_mask` plus a no-receiver guard gates the call side; - without its regex state, `/invoke('save_config')/` — a legal regex literal the - base extractor records nothing for — becomes a fabricated edge to a real command. - The same Rust mask also gates detection probe 4, without which THIS repository - self-detects as a Tauri project off the literal in its own source. -- **Known limitations, deliberate.** References are FILE-granular - (`from_node_id = file:`), so `codegraph callers get_mcp_port` answers - with `src/app.ts` rather than the enclosing function; reconstructing that id is - unsafe here because TypeScript has many declaration forms and an object-literal - call site has no enclosing function node at all. `invoke(cmdName)`, - ``invoke(`cmd_${id}`)`` and `client.invoke('x')` emit nothing, with no dynamic - sentinel — Tauri has no `codegraph audit`-style consumer that would read one. - `generate_handler![…]` is NOT parsed, so an attributed-but-unregistered command - still gets an edge (asserted deliberately). Events (`listen`/`once`) and the typed - `commands.getMcpPort()` binding path are out of scope. - -## MCP protocol surface (`codegraph-mcp`, rmcp 3.0.1) - -`codegraph-mcp` builds on **rmcp 3.0.1** (`crates/codegraph-mcp/Cargo.toml`, dep and dev-dep; -features `server`, `client`, `transport-io`, `transport-streamable-http-server`). The 2.1 → 3.0.1 -upgrade cost three lines in `rmcp_handler.rs`: `call_tool` returns the `#[non_exhaustive]` -`CallToolResponse` enum instead of `CallToolResult` (we only ever build `Complete`, via `.into()`), -`with_stateful_mode` became `with_legacy_session_mode`, and a stale `get_info()` comment was -corrected. The 3.x breaking changes that bit third-party code were all CLIENT-side -(`InitializeResult` → `ServerPeerInfo`, optional `server_info`, OAuth `resolve_metadata()`), none of -which touch a server `ServerHandler`. `Peer::list_roots` is still `#[deprecated]` (SEP-2577) with -still no replacement, so the `#[allow(deprecated)]` at `rmcp_handler.rs:334` stays. - -**We serve five protocol revisions, and that is the SDK default rather than a choice.** -`ProtocolVersion` is `struct ProtocolVersion(Cow<'static, str>)` plus associated constants (NOT an -enum); rmcp's `negotiate_protocol_version` echoes back any client-requested version present in -`KNOWN_VERSIONS`. So `get_info()`'s `.with_protocol_version(V_2024_11_05)` is only the FALLBACK for -an unknown client version — it is not a ceiling, and we do not pin or force 2024-11-05: - -| client requests | negotiated | `resultType` in results | HTTP session | -| --------------- | ---------- | ----------------------- | ---------------------- | -| 2024-11-05 | 2024-11-05 | absent | no header (our config) | -| 2025-03-26 | 2025-03-26 | absent | no header (our config) | -| 2025-06-18 | 2025-06-18 | absent | no header (our config) | -| 2025-11-25 | 2025-11-25 | absent | no header (our config) | -| 2026-07-28 | 2026-07-28 | `"complete"` | no header (per spec) | - -`resultType` (SEP-2322) is absent for pre-2026 peers because upstream -[#1038](https://github.com/modelcontextprotocol/rust-sdk/pull/1038) strips it for legacy peers, and -2026-07-28 is stateless per SEP-2567 — no `Mcp-Session-Id` — unconditionally, regardless of -`with_legacy_session_mode`, which now governs legacy versions only. We pass -`with_legacy_session_mode(false)` (`rmcp_handler.rs:737`), so the legacy versions issue no -`Mcp-Session-Id` either: THIS server never sends one at any revision. The two absences differ in -cause, though — legacy is our configuration and would come back if that flag flipped to `true`, -while 2026-07-28 is mandated and cannot be turned back on. SEP-2243 standard headers are -validated in both directions: matching `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` reach the -tool, while a missing protocol header or a mismatched `Mcp-Method` / `Mcp-Name` is rejected with -HTTP 400 + JSON-RPC code `-32020`. Coverage lives in `tests/rmcp_l3.rs` (version echo, `resultType` -presence/absence) and `tests/rmcp_http.rs` (no session header, SEP-2243 both branches). - -**Not implemented, deliberately:** Tasks (SEP-2663), MRTR / elicitation-in-tool, and subscriptions. -We advertise only the tools capability and always return `CallToolResponse::Complete`; we never -construct `Task` or `InputRequired`. `accepted_subscription_filter` keeps its `None` default, while -legacy `subscribe` / `unsubscribe` return method-not-found. The request surface is not literally -tools-only, however. Beyond the mandatory `initialize` handshake — itself an inherited default -(`handler/server.rs:314-326`), which is why negotiation runs automatically over our `get_info()` and -why the `V_2024_11_05` there is only a fallback — these inherited defaults also answer `Ok`. -`discover` returns real content -(`supported_protocol_versions()` plus `get_info()`); `complete` returns an empty `CompleteResult`; -`list_prompts` / `list_resources` / `list_resource_templates` (`handler/server.rs:362-383`) return -EMPTY results, so `prompts/list`, `resources/list`, and `resources/templates/list` answer with -nothing rather than method-not-found; and `ping` succeeds on the legacy revisions only -(method-not-found at 2026-07-28, `handler/server.rs:112-118`). The `set_level`, `get_prompt`, -`read_resource`, and task-lifecycle defaults return method-not-found. These are SDK defaults, not -our implementations, and they add no advertised capabilities. - -**MCP golden fixtures are STRUCTURAL, not byte-stable.** `tests/support/parity.rs:244-300` and -`tests/golden_mcp.rs` compare only NAMED fields — initialize: `protocolVersion`, `capabilities`, -`serverInfo.name`, `serverInfo.version`, `instructions`; tools/list: names + order + `inputSchema` + -annotations; tool result: `content[0].type`, `isError`, and the sorted set of non-empty text lines; -error: `code` + `message` — never whole-object equality. That is why an added top-level `resultType` -could not drift the 15 existing fixtures, and equally why it needed its own explicit test. (The -EXTRACTION goldens under `reference/golden/` are a different contract and genuinely ARE -byte-stable — see "Hard invariants" above.) - -## HTTP MCP server: background mode + addr-keyed registry - -`serve --mcp` (stdio) uses the PER-PROJECT daemon (`.codegraph/daemon.pid` + socket; the whole -rendezvous is derived from `IndexPaths::current_root`). `serve --http` -(streamable-HTTP) is different: HTTP servers are keyed by BIND ADDR — a global server (no `--path`) -spans many projects — so they use a GLOBAL, addr-keyed registry, NOT the per-project root. The registry -lives in `codegraph-daemon/src/http_registry.rs`: one `.json` file per running server -(`HttpServerInfo { pid, addr, mode, project, started_at, version, log_file }`) under -`$XDG_STATE_HOME/codegraph/http` (else `~/.local/state/codegraph/http`; `%LOCALAPPDATA%\codegraph\http` -on Windows; `CODEGRAPH_HTTP_REGISTRY_DIR` overrides). Entries are pruned when their pid is dead -(self-heal, gated on `is_process_alive`). - -`serve --http` stays FOREGROUND by default; `serve --http --detach` runs in the BACKGROUND via -`spawn::spawn_detached_http` (generalized from `spawn_detached_daemon` over the shared `detach()` -primitive; the child carries `CODEGRAPH_HTTP_DETACH_INTERNAL=1` so it runs the foreground serve path -and does NOT re-detach). On startup `serve --http` prunes dead entries, ERRORS on a live same-addr -conflict (listing the running instance), and notes any other live servers when the addr is free. The -`codegraph http {list, status, stop}` subcommand group inspects and terminates registered servers -(`stop` uses `process::terminate_pid` — SIGTERM on unix / `TerminateProcess` on Windows). None of this -touches extraction/golden equivalence. - -Foreground stdio `serve --mcp` gets a THIRD, PID-keyed registry -(`codegraph-daemon/src/mcp_registry.rs`): one `.json` -(`McpServerInfo { pid, project: Option, transport: "stdio", started_at, version }`, camelCase on -the wire) under the same GLOBAL state chain as the HTTP one but with an `mcp` leaf -(`$XDG_STATE_HOME/codegraph/mcp`, else `~/.local/state/codegraph/mcp`, `%LOCALAPPDATA%\codegraph\mcp` on -Windows; `CODEGRAPH_MCP_REGISTRY_DIR` overrides). PID keying is forced by the transport: a stdio process -has no addr and no per-project rendezvous, several may serve one project, and one may serve none. -Registration fires from all THREE foreground exits in `cmd_serve` (`Direct`, `SpawnOrProxy`, and the -too-broad-root home guard) and NEVER from `BeDaemon`, which already owns `.codegraph/daemon.pid`. -Reads go through `RegistryRead::{Available, Unavailable}` so a MISSING directory ("nobody registered -yet", normal) is distinguishable from an unreadable one (an outage); `read_dir`'s `NotFound` only means -MISSING when `fs::symlink_metadata` also fails, so a dangling symlink at the registry path reads as an -outage instead of an empty registry. `list_entries` is the RAW on-disk view (stale entries included); -`live_entries` filters its RETURN VALUE by `is_process_alive` rather than trusting `prune_dead`'s -deletions to have landed, so a dead entry on an undeletable (read-only) registry is still never reported -as running — dead-PID and unparseable files are pruned on read as best-effort disk self-heal only. -`project` records the launch `--path` and ONLY that: it is `Some` only when the user actually passed -`--path` (a bare `serve --mcp` stores `None`, not its cwd), and it is purely INFORMATIONAL — never a -capability boundary, because `roots::resolve_project_arg` probes an absolute per-call `projectPath` on its -own merits and consults the launch default only when no path was passed, so any live server can be asked -to open any indexed project's database. `codegraph mcp list [--json]` renders it as `LAUNCH PROJECT`, and -BOTH holder diagnostics — `index`'s pre-warning and the `RemoveDatabase` FAILURE path — therefore report -ALL live entries with no narrowing: filtering by `project` would drop the holder in the very case they -exist for (a server launched elsewhere that a client has since pointed at this project), and the failure -path additionally has only a DB path. `CODEGRAPH_DIR` selects one validated -project-local directory name, so the database remains inside the project tree. This -registry is PURE OBSERVABILITY — there is no `mcp stop` and no `terminate_pid` import, because a stale -entry's PID may have been reused and this workspace has no portable instance-identity primitive -(`try_acquire_daemon_lock` is a `create_new` placeholder plus a recorded PID, not an OS advisory lock); -`list` asks a human to confirm the PID with `ps -p -o command=` / `tasklist /FI "PID eq "` -before offering `kill ` / `taskkill /PID /F`. CLI/daemon only; extraction and golden -equivalence untouched. - -## Agent installer (`codegraph install` / `uninstall`) - -`codegraph install` writes the codegraph MCP-server entry into each supported agent's config -(Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, Trae, Qoder, Zed); -`uninstall` reverses it. The written command launches the binary (`command: "codegraph"`, -`args: ["serve", "--mcp"]`). Cursor and Trae use `--path ${workspaceFolder}` in their global config so -one entry auto-follows each project window; Kiro and Qoder write a bare global entry (no `--path`) that -serves tools read-only off any existing index, with the agent passing the project path per call — run -`codegraph init --target=` inside each project to write a project-local config with an absolute -`--path` for live watch. Kiro's `mcp.json` also carries a `//`-commented HTTP alternative alongside the -active stdio entry (JSONC, idempotent, injected best-effort without corrupting existing files); it uses -`http://localhost:8111/mcp` because Kiro allows `http` only for localhost (remote servers must be `https`). -Zed's `settings.json` likewise carries `//`-commented remote-development alternatives after the active -`context_servers.codegraph` stdio entry (both `install` global and `init` project-local): an SSH-stdio -bridge and an HTTP server (`http://localhost:8111/mcp`, marked RECOMMENDED for remote); the shared -JSONC-safe injector is `inject_commented_alternative(path, parent_key, entry_key, sentinel, block)` in -`shared.rs`, used by both Kiro (`mcpServers`) and Zed (`context_servers`). -Non-interactive, flag-driven (`--target`, `--global`/`--local`/`--location`, -`--yes`, `--no-permissions`, `--print-config`); the config-writing logic (paths/keys/marker sections, -idempotent upsert, uninstall removal) is CLI-only and additive — it does NOT touch -extraction/golden equivalence. - -## Verification gates (run before every commit) +Use `explore` for an area or call flow, `search` for a known name, `node` for one +symbol/file plus its trail, and `impact` before a refactor. Do not append `.` as +a second positional argument to a research command. If the index is unavailable, +follow `status` recovery guidance; do not initialize or rebuild unless requested +or required by that guidance. + +## Hard invariants + +1. **Deterministic graph output.** Identical source and configuration must produce + identical canonical nodes, edges, references, files, and query ordering. + Incremental `sync` must converge with a clean full index. +2. **Golden compatibility.** Extraction goldens under `reference/golden/` are + byte-stable canonical artifacts. Update them only for an intentional graph + behavior change, with the matching corpus and regeneration evidence documented + in [`docs/equivalence.md`](docs/equivalence.md). +3. **Stable node IDs.** Symbol IDs are + `{kind}:{sha256("{filePath}:{kind}:{name}:{line}").hex[..32]}`; file nodes are + `file:{relative/path}`. Paths use `/`; lines are 1-based. Never change this + formula incidentally. +4. **No AI/vector runtime.** Do not add AI, LLM, embedding, vector-database, or + inference dependencies. `scripts/guardrail.sh` enforces the dependency boundary. +5. **Project containment.** Managed state stays under the selected project index + root. Filesystem fallbacks must prove lexical containment before probing or + reading a path. Do not broaden project authority through ancestor discovery, + symlink guessing, environment-global state, or another project's configuration. +6. **Fail closed.** Ambiguous resolution stays unresolved. Unsafe stale source is + served whole or omitted, never sliced using stale line ranges. Lock, checksum, + migration, and release validation failures must stop rather than silently skip. +7. **Protocol and stdout purity.** MCP/JSON-RPC output owns stdout. Logs and + diagnostics go to stderr. Additive fields are preferred; existing JSON, text, + installer, and protocol contracts require explicit compatibility tests. +8. **No manual releases or version edits.** Release Please owns versions and tags. + Distribution is GitHub Releases plus `cargo install --git`; no crate is + published to crates.io. + +## Workspace ownership + +The workspace members are declared in root `Cargo.toml`; that manifest is the +authority when the list changes. + +| Crate | Owns | +| -------------------------------- | ------------------------------------------------------------------ | +| `codegraph-core` | shared types, config, IDs, file classification, logging | +| `codegraph-extract` | language detection, tree-sitter/custom extraction, scan policy | +| `codegraph-store` | SQLite schema/migrations, FTS5, persistence and queries | +| `codegraph-resolve` | import/name resolution and framework resolvers | +| `codegraph-graph` | traversal, impact, search scoring and query parsing | +| `codegraph-mcp` | MCP schemas, rmcp transports, project resolution, tool rendering | +| `codegraph-watch` | incremental synchronization and filesystem watching | +| `codegraph-daemon` | shared process lifecycle, IPC, registries and detach behavior | +| `codegraph-cli` (`codegraph-rs`) | CLI, installer, orchestration and shipped binary | +| `codegraph-bench` | equivalence oracle and reproducible benchmark harness; not shipped | + +Keep dependency direction acyclic and lower layers independent of presentation. +Extraction must not depend on store/graph. Query rendering must not leak into core +semantics. See [`docs/architecture.md`](docs/architecture.md) for the current graph. + +## Change-to-proof matrix + +Run the narrowest relevant tests while iterating, then the complete gate before +handoff. Do not use a narrow test to claim a workspace-wide property. + +| Change | Minimum focused proof | Required documentation | +| -------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| extraction/language rules | extractor tests; affected golden corpus; incremental/full equivalence | `languages.md`, `grammar-manifest.md`, and golden recipe when behavior moves | +| resolution/framework rules | resolver unit/integration tests; ambiguity negatives; affected golden | `equivalence.md` or framework reference when public behavior moves | +| schema/migrations/store | schema parity; migration replay; state/lease tests; golden equivalence | `data-model.md` | +| graph/search | graph/query tests; deterministic ordering and limit cases | CLI/MCP docs for public output changes | +| MCP/protocol | engine tests; structural MCP goldens; rmcp stdio/HTTP/version tests | `mcp.md` | +| CLI/installer | command tests; installer round trips; script contract fixtures | `cli.md`, README only for landing-page behavior | +| daemon/watch/concurrency | lifecycle, lock, recovery, watcher and platform-focused tests | architecture/CLI/MCP lifecycle sections | +| release/install/checksum | shell/PowerShell fixtures, asset-name checks, archive smoke | README install section and release workflow contract | +| docs/community files | `python3 scripts/docs-check.py`; formatter; link/anchor checks | update the canonical page, not a duplicate summary | + +If a change alters nodes, edges, reference resolution, file classification, or +stored graph meaning, decide explicitly whether the extraction version must move. +Group related graph-semantic changes so users do not rebuild repeatedly. Schema +version and extraction version are independent. + +## Documentation rules + +- Public technical docs are canonical in English. The Chinese README is a + maintained landing-page mirror, not a promise to translate every deep reference. +- Docs describe **AS-BUILT** behavior. Update the relevant page in the same change + as code. Avoid fixed counts and point-in-time versions unless a source-contract + test derives and checks them. +- Keep the English and Chinese README structure, commands, security claims, and + links in sync. Move volatile CLI/IDE/protocol details into canonical docs. +- Historical entries in `docs/upstream-sync/UPSTREAM.md` and dated audit files are + evidence: append a new entry; never rewrite an old observation to look current. +- Do not publish unmeasured performance claims. Benchmark method and results must + identify the commit, corpus, environment, command, run count, and dispersion. +- `CLAUDE.md` must remain a regular file containing exactly `@AGENTS.md` plus a + trailing newline. + +Directory-specific rules live in [`docs/AGENTS.md`](docs/AGENTS.md). + +## Validation + +The authoritative local/CI entry point is: +```bash +make check ``` -make ci # fmt-check + clippy + test + guardrail -# or individually: -cargo test --workspace # incl. golden oracle + sync equivalence -cargo clippy --workspace --all-targets -- -D warnings + +`make ci` is a compatibility alias for the same complete gate. `make pre-ci` adds a package/unpack/execute smoke over the built release bytes. +It validates workspace-version consistency before any Cargo subprocess, required +tool versions, Rust and repository-text formatting, workflow/shell linting, +Clippy with warnings denied, locked tests, a locked release build, guardrails, +and script fixtures. The version-controlled pre-push hook calls this same path. + +Useful focused commands: + +```bash +cargo test -p --locked +cargo test -p codegraph-bench --test equivalence --locked +cargo test -p codegraph-mcp --locked cargo fmt --all --check +cargo clippy --workspace --all-targets --locked -- -D warnings +python3 scripts/docs-check.py +actionlint .github/workflows/*.yml bash scripts/guardrail.sh -make coverage # workspace coverage summary (informational; `make coverage-html` for the full report) ``` -## Test coverage (tracked, informational) - -- Unit-test coverage is a tracked metric via `cargo llvm-cov` + Codecov. - Run `make coverage` for a summary; `make coverage-html` for the browsable - report; `make coverage-lcov` writes the `lcov.info` CI uploads. -- **Target is 95%+** (aspirational). The CI gate is **informational / - non-blocking** — the `coverage` job is kept out of the `CI Success` gate and - the Codecov status is `informational: true` (`codecov.yml`), so a below-target - % never turns CI red. This honors the iron rule "local green ⇒ CI green". -- **Measured 94.42% line / 94.12% region / 93.41% function** — close to the 95% - target but not yet there. Figure measured at `4fabc0b` (rmcp 3.0.1 round) via - `make coverage`; re-measure before quoting it, since it moves with every round. - The remaining gap is now concentrated in the **per-language extractors** rather - than the resolver or watch internals: 7 of the 10 lowest-covered files are - `codegraph-extract/src/lang/*` — `r.rs` 70.59%, `dart.rs` 72.75%, - `erlang.rs` 75.77%, `arkts.rs` 81.01%, `nix.rs` 82.21%, `jsx.rs` 82.61%, - `lua.rs` 85.94% — alongside `codegraph-watch/src/migrate.rs` 74.77%, - `codegraph-store/src/index_state.rs` 80.45%, and - `codegraph-store/src/connection.rs` 82.57%. -- **Enabling Codecov:** enable the repo at codecov.io. This repo is public, so - tokenless upload works (no `CODECOV_TOKEN` needed); a private repo would need - `CODECOV_TOKEN` in GitHub repo Secrets. - -## CI, hooks & release - -- **Pre-push hook** (`.githooks/pre-push`): runs fmt + clippy + test + guardrail - on `git push` (never on commit). Enable once per clone with `make hooks` - (sets `core.hooksPath`). Local green ⇒ CI green. -- **CI** (`.github/workflows/ci.yml`): `Test` (fmt/clippy/test/guardrail) + - `Security Audit` (cargo-audit) + `CI Success` gate, on push/PR to `main`. -- **Release** (`.github/workflows/release-please.yml`): release-please opens a - release PR; merging it cuts a `v` tag and triggers the pipeline — - 6-platform binaries (linux musl x86_64/aarch64 via cargo-zigbuild, macOS - x86_64/aarch64, and Windows MSVC `x86_64-pc-windows-msvc` on `windows-latest` - plus `aarch64-pc-windows-msvc` on `windows-11-arm`), git-cliff release notes, - and a GitHub Release with the binaries attached. The project is distributed via GitHub Releases + - `cargo install --git`; it is NOT published to crates.io. Version bumps are - owned by release-please via `.release-please-manifest.json` — never bump by - hand. -- **Commits are English Conventional Commits.** `feat`→minor, `fix`→patch. - The end-to-end release runbook lives in the `codegraph-release` skill. -- **Docs** are formatted with `oxfmt` (`make fmt`); `.oxfmtignore` excludes - golden fixtures, embedded JSON, and auto-generated files. +Coverage is tracked against an aspirational 95% target but remains informational +until it is sustainably above that target. Do not weaken tests to raise coverage, +and do not quote a cached percentage as current without re-measuring it. + +## Upstream synchronization + +The TypeScript reference is `colbymchenry/codegraph`; the Rust product remains +independent. Read [`docs/upstream-sync/UPSTREAM.md`](docs/upstream-sync/UPSTREAM.md) +first. Port behavior and intent, not TypeScript mechanisms. For each upstream +family record one of `PORT`, `ALREADY-HAVE`, `N/A`, or `DEFER`, with source proof, +Rust target, graph/schema/API impact, and acceptance tests. Update the ledger only +after current source and runtime evidence agree. + +## Git, PR, and release discipline + +- Preserve dirty work and owner-uncertain worktrees. Use an isolated worktree for + implementation; never reset, clean, overwrite, apply/drop stashes, or delete + another worker's state. +- Commits and PR titles use English Conventional Commits. `feat` is a minor bump, + `fix` is a patch bump, and `feat!`/`BREAKING CHANGE` is major. Never add AI or + co-author trailers. +- Stage specific files. Do not commit, push, merge, or mutate GitHub settings + unless explicitly requested. +- The required check is `CI Success`. Coverage is separately informational. +- Release runs are same-run, draft-until-verified: the exact tag SHA, six platform + archives, archive smoke, checksums, attestations, asset inventory, and source CI + gate must pass before publication. Never describe a draft or partial run as a + release. +- A release claim binds implementation PR head → merge → tag SHA → workflow run → + downloaded public bytes. “Latest” is not evidence. + +Canonical details: [`CONTRIBUTING.md`](CONTRIBUTING.md), +[`docs/equivalence.md`](docs/equivalence.md), +[`docs/mcp.md`](docs/mcp.md), and [`docs/upstream-sync/`](docs/upstream-sync/). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..9d68a666 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment include: + +- demonstrating empathy and kindness toward other people; +- respecting differing opinions, viewpoints, and experiences; +- giving and gracefully accepting constructive feedback; +- accepting responsibility and apologizing to those affected by our mistakes; +- focusing on what is best for the overall community. + +Examples of unacceptable behavior include: + +- sexualized language or imagery, and sexual attention or advances; +- trolling, insulting or derogatory comments, and personal or political attacks; +- public or private harassment; +- publishing others' private information without explicit permission; +- other conduct that could reasonably be considered inappropriate in a + professional setting. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing standards of +acceptable behavior and will take appropriate and fair corrective action in +response to behavior they deem inappropriate, threatening, offensive, or +harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned with this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces and when an individual +is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported privately through the repository maintainer's GitHub profile or the +repository's private security-advisory channel when confidentiality is needed. +All complaints will be reviewed and investigated promptly and fairly. Community +leaders must respect the privacy and security of reporters. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines: + +1. **Correction:** A private, written warning for inappropriate language or + behavior, explaining the nature of the violation. +2. **Warning:** A warning with consequences for continued behavior, including no + interaction with the people involved for a specified period. +3. **Temporary Ban:** A temporary ban from interaction or public communication + with the community. +4. **Permanent Ban:** Permanent removal for a pattern of violations or severe + harassment or aggression. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, +available at . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..de738cfb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contributing to CodeGraph-Rust + +Thank you for improving CodeGraph. The project values deterministic behavior, +small evidence-backed changes, and explicit static-analysis boundaries. + +## Before you start + +1. Read [`AGENTS.md`](AGENTS.md), especially the hard invariants and proof matrix. +2. Search existing issues and the upstream ledger before duplicating work. +3. Use an isolated branch/worktree; preserve unrelated local work. +4. For large graph-semantic, schema, protocol, or release changes, open an issue + or draft PR first so the compatibility boundary is explicit. + +By participating, you agree to follow the +[Code of Conduct](CODE_OF_CONDUCT.md). Report vulnerabilities privately as +specified in [`SECURITY.md`](SECURITY.md), not in a public issue. + +## Development setup + +The repository pins Rust and all shipped dependencies. Install the non-Rust tools +reported by `make help`/`make tools-check`, then enable the versioned hook: + +```bash +git clone https://github.com/sunerpy/codegraph-rust.git +cd codegraph-rust +make hooks +make check +``` + +Do not replace locked commands with unlocked equivalents. `make ci` aliases `make check`; `make pre-ci` adds an archive package/unpack/execute smoke. + +## Work on a change + +- Start with the narrowest test that demonstrates the behavior. +- Keep extraction, storage, resolution, graph algorithms, and presentation in + their owning crates. +- Add ambiguity and negative cases. A false edge is often worse than an + unresolved reference. +- Update the canonical documentation in the same change as public behavior. +- Run `python3 scripts/docs-check.py` after changing Markdown links, headings, + README structure, schema/language claims, or agent guides. + +### Golden and version-sensitive changes + +If nodes, edges, unresolved references, file classification, or resolution +meaning changes: + +1. identify the affected corpus or add the smallest focused corpus; +2. decide explicitly whether the extraction version must change; +3. regenerate with the recipe in [`docs/equivalence.md`](docs/equivalence.md); +4. review every changed canonical row and schema statement; +5. prove incremental `sync` and clean indexing converge. + +Never hand-edit canonical golden JSON to make a test pass. SQLite `.db` files are +inputs to canonicalization and are not themselves byte-reproducible. + +Schema migrations must be forward-safe, replay-tested, and reflected in +[`docs/data-model.md`](docs/data-model.md). Schema version and extraction version +are separate decisions. + +## Commit and PR conventions + +Use English Conventional Commits: + +```text +feat(scope): imperative summary +fix(scope): imperative summary +``` + +`feat` drives a minor release and `fix` a patch release. Breaking changes use +`feat!` and/or a `BREAKING CHANGE:` footer. Keep the subject imperative, omit a +trailing period, and do not add AI/co-author trailers. + +A PR should state: + +- the problem and observed evidence; +- the chosen behavior and static/runtime boundary; +- compatibility, schema, extraction-version, and golden impact; +- focused and full validation actually run; +- documentation updated; +- remaining risks or intentionally deferred work. + +Complete the PR template. Keep unrelated formatting or refactors out of the +diff. Maintainers may ask for a minimal reproduction before accepting a new +heuristic. + +## Validation + +During iteration, run affected crate/tests. Before handoff, run: + +```bash +make check +git diff --check +``` + +The complete gate includes locked tests, Clippy with warnings denied, release +build, formatting, workflow/shell lint, repository guardrails, and script +fixtures. Coverage is informational; never delete or weaken tests to move a +percentage. + +## Releases + +Do not edit workspace versions, the release manifest, the scaffold version +placeholder, tags, or published release assets manually. Release Please owns +all four version surfaces. Maintainers publish only through +the draft-until-verified workflow after exact-SHA CI, all platform archives, +checksums, attestations, and archive smoke pass. diff --git a/Cargo.lock b/Cargo.lock index 076c0cbc..71231295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -1043,7 +1043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1958,7 +1958,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2203,7 +2203,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2555,14 +2555,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", @@ -2614,7 +2614,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2625,9 +2625,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -3035,7 +3035,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4082,7 +4082,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 503fe97a..4a6f2b2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ resolver = "2" [workspace.package] version = "0.50.3" +publish = false edition = "2024" license = "MIT" authors = ["sunerpy "] diff --git a/Makefile b/Makefile index 16103d99..08e8a7d2 100644 --- a/Makefile +++ b/Makefile @@ -1,250 +1,201 @@ -.PHONY: all build build-dev build-prod release release-target install uninstall \ - fmt fmt-rust fmt-oxfmt fmt-check fmt-rust-check fmt-oxfmt-check \ - lint check test guardrail ci hooks setup-hooks clean size-compare help \ - coverage coverage-html coverage-lcov coverage-open coverage-clean +SHELL := /bin/sh +.DEFAULT_GOAL := help -# Project configuration PROJECT_NAME := codegraph-rs -BINARY_NAME := codegraph -CLI_CRATE := codegraph-rs -CARGO := cargo - -# Directories +BINARY_NAME := codegraph +CLI_CRATE := codegraph-rs +CARGO ?= cargo TARGET_DIR := target -DIST_DIR := dist - -# Cross-compilation target (set via environment variable or command line) -# Example: make release-target TARGET=x86_64-unknown-linux-musl +DIST_DIR := dist TARGET ?= +OXFMT_VERSION := 0.64.0 +ACTIONLINT_VERSION := 1.7.12 +OXFMT_ARGS := --no-error-on-unmatched-pattern --ignore-path .oxfmtignore . + +.PHONY: all build build-dev build-prod release release-target install uninstall \ + fmt fmt-rust fmt-oxfmt fmt-check fmt-rust-check fmt-oxfmt-check \ + tools-check workflow-lint docs-check workspace-version typecheck lint test guardrail script-tests archive-smoke \ + check ci pre-ci hooks setup-hooks clean size-compare help \ + coverage coverage-html coverage-lcov coverage-open coverage-clean -# Default target all: build -# Build the CLI in debug mode and copy the binary into dist/ build: build-dev build-dev: - @echo "🔨 Building $(PROJECT_NAME) (debug)..." - $(CARGO) build -p $(CLI_CRATE) + @echo "Building $(PROJECT_NAME) (debug)..." + $(CARGO) build --locked -p $(CLI_CRATE) @mkdir -p $(DIST_DIR) - @cp $(TARGET_DIR)/debug/$(BINARY_NAME) $(DIST_DIR)/$(BINARY_NAME) 2>/dev/null || \ - echo "⚠️ Binary not found, check Cargo.toml [[bin]] configuration" - @echo "✅ Debug build complete: $(DIST_DIR)/$(BINARY_NAME)" + @if [ -f "$(TARGET_DIR)/debug/$(BINARY_NAME)" ]; then \ + cp "$(TARGET_DIR)/debug/$(BINARY_NAME)" "$(DIST_DIR)/$(BINARY_NAME)"; \ + elif [ -f "$(TARGET_DIR)/debug/$(BINARY_NAME).exe" ]; then \ + cp "$(TARGET_DIR)/debug/$(BINARY_NAME).exe" "$(DIST_DIR)/$(BINARY_NAME).exe"; \ + else echo "binary not found"; exit 1; fi build-prod: release -# Build the optimized release binary and copy it into dist/ release: - @echo "🚀 Building $(PROJECT_NAME) (release)..." - $(CARGO) build --release -p $(CLI_CRATE) + @echo "Building $(PROJECT_NAME) (release)..." + $(CARGO) build --locked --release -p $(CLI_CRATE) @mkdir -p $(DIST_DIR) - @cp $(TARGET_DIR)/release/$(BINARY_NAME) $(DIST_DIR)/$(BINARY_NAME) 2>/dev/null || \ - echo "⚠️ Binary not found, check Cargo.toml [[bin]] configuration" - @echo "✅ Release build complete: $(DIST_DIR)/$(BINARY_NAME)" - @ls -lh $(DIST_DIR)/$(BINARY_NAME) 2>/dev/null || true + @if [ -f "$(TARGET_DIR)/release/$(BINARY_NAME)" ]; then \ + cp "$(TARGET_DIR)/release/$(BINARY_NAME)" "$(DIST_DIR)/$(BINARY_NAME)"; \ + elif [ -f "$(TARGET_DIR)/release/$(BINARY_NAME).exe" ]; then \ + cp "$(TARGET_DIR)/release/$(BINARY_NAME).exe" "$(DIST_DIR)/$(BINARY_NAME).exe"; \ + else echo "binary not found"; exit 1; fi -# Build the release binary for a specific target (cross-compilation). -# Usage: make release-target TARGET=x86_64-unknown-linux-musl release-target: ifndef TARGET $(error TARGET is not set. Usage: make release-target TARGET=x86_64-unknown-linux-musl) endif - @echo "🚀 Building $(PROJECT_NAME) for target $(TARGET)..." - $(CARGO) build --release -p $(CLI_CRATE) --target $(TARGET) + $(CARGO) build --locked --release -p $(CLI_CRATE) --target "$(TARGET)" @mkdir -p $(DIST_DIR) @if [ -f "$(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME)" ]; then \ - cp $(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME) $(DIST_DIR)/$(BINARY_NAME)-$(TARGET); \ + cp "$(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME)" "$(DIST_DIR)/$(BINARY_NAME)-$(TARGET)"; \ elif [ -f "$(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME).exe" ]; then \ - cp $(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME).exe $(DIST_DIR)/$(BINARY_NAME)-$(TARGET).exe; \ - else \ - echo "⚠️ Binary not found for target $(TARGET)"; \ - exit 1; \ - fi - @echo "✅ Release build complete for $(TARGET)" - @ls -lh $(DIST_DIR)/$(BINARY_NAME)-$(TARGET)* 2>/dev/null || true - -# Install the binary to ~/.cargo/bin + cp "$(TARGET_DIR)/$(TARGET)/release/$(BINARY_NAME).exe" "$(DIST_DIR)/$(BINARY_NAME)-$(TARGET).exe"; \ + else echo "binary not found for $(TARGET)"; exit 1; fi + install: release - @echo "📦 Installing $(BINARY_NAME) to ~/.cargo/bin..." - $(CARGO) install --path crates/codegraph-cli - @echo "✅ Installation complete" + $(CARGO) install --locked --path crates/codegraph-cli uninstall: - @echo "🗑️ Uninstalling $(CLI_CRATE)..." $(CARGO) uninstall $(CLI_CRATE) || true -# oxfmt: formats Markdown / JSON / YAML etc. (Rust is owned by `cargo fmt`). -# Auto-generated files (CHANGELOG.md, release-please manifests) + golden -# fixtures are excluded via .oxfmtignore. -OXFMT := oxfmt -OXFMT_ARGS := --no-error-on-unmatched-pattern --ignore-path .oxfmtignore . - -# Format code (Rust via cargo fmt + docs/markdown via oxfmt) -fmt: fmt-rust fmt-oxfmt - @echo "✨ Formatting complete." +# Every formatter/linter target fails closed when its tool is absent. CI installs +# the exact pinned versions before invoking the same make check entry point. +tools-check: + @command -v jq >/dev/null 2>&1 || { echo "jq is required"; exit 1; } + @command -v python3 >/dev/null 2>&1 || { echo "python3 is required"; exit 1; } + @python3 -c 'import yaml' >/dev/null 2>&1 || { echo "PyYAML is required"; exit 1; } + @command -v oxfmt >/dev/null 2>&1 || { echo "oxfmt $(OXFMT_VERSION) is required: npm install --global oxfmt@$(OXFMT_VERSION)"; exit 1; } + @test "$$(oxfmt --version | awk '{print $$NF}')" = "$(OXFMT_VERSION)" \ + || { echo "oxfmt $(OXFMT_VERSION) is required: npm install --global oxfmt@$(OXFMT_VERSION)"; exit 1; } + @command -v actionlint >/dev/null 2>&1 || { echo "actionlint $(ACTIONLINT_VERSION) is required"; exit 1; } + @test "$$(actionlint -version | head -1 | sed 's/^v//')" = "$(ACTIONLINT_VERSION)" \ + || { echo "actionlint $(ACTIONLINT_VERSION) is required"; exit 1; } + @command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck is required"; exit 1; } + +fmt: tools-check fmt-rust fmt-oxfmt + @echo "Formatting complete." fmt-rust: - @echo "✨ Formatting Rust (cargo fmt)..." $(CARGO) fmt --all fmt-oxfmt: - @echo "✨ Formatting docs (oxfmt)..." - @if command -v $(OXFMT) >/dev/null 2>&1; then \ - $(OXFMT) $(OXFMT_ARGS); \ - else \ - echo "⚠️ oxfmt not found — skipping doc formatting. Install: npm i -g oxfmt"; \ - fi + oxfmt --write $(OXFMT_ARGS) -# Check code formatting (CI: Rust + docs) -fmt-check: fmt-rust-check fmt-oxfmt-check - @echo "✨ Format check complete." +fmt-check: tools-check fmt-rust-check fmt-oxfmt-check + @echo "Format check complete." fmt-rust-check: - @echo "✨ Checking Rust formatting..." $(CARGO) fmt --all --check fmt-oxfmt-check: - @echo "✨ Checking doc formatting..." - @if command -v $(OXFMT) >/dev/null 2>&1; then \ - $(OXFMT) --check $(OXFMT_ARGS); \ - else \ - echo "⚠️ oxfmt not found — skipping doc format check. Install: npm i -g oxfmt"; \ - fi - -# Run linter (clippy, warnings denied) -lint: - @echo "🔍 Running clippy..." - $(CARGO) clippy --workspace --all-targets -- -D warnings + oxfmt --check $(OXFMT_ARGS) -# Type-check the workspace without producing artifacts -check: - @echo "✅ Checking workspace..." - $(CARGO) check --workspace +workspace-version: + @bash scripts/check-workspace-versions.sh + +typecheck: + $(CARGO) check --workspace --locked + +lint: + $(CARGO) clippy --workspace --all-targets --locked -- -D warnings -# Run the full test suite (incl. golden oracle + equivalence) test: - @echo "🧪 Running tests..." - $(CARGO) test --workspace + $(CARGO) test --workspace --locked + +workflow-lint: tools-check + actionlint .github/workflows/*.yml + shellcheck .github/scripts/*.sh .githooks/pre-push scripts/*.sh scripts/tests/*.sh + +docs-check: tools-check + python3 scripts/docs-check.py -# Scope guardrail: no AI / vector / LLM crates in the workspace, and no drift -# between the release workflow's asset names and what the installers download. +# Guardrail includes deterministic workflow/installer contract checks. guardrail: - @echo "🛡️ Running scope guardrail..." - bash scripts/guardrail.sh + @bash scripts/guardrail.sh -# Run every gate that CI enforces -ci: fmt-check lint test guardrail - @echo "✅ All CI checks passed!" +script-tests: + @for test_script in scripts/tests/*.test.sh; do \ + echo "Running $$test_script"; bash "$$test_script"; \ + done -# ─── Test coverage (cargo-llvm-cov + Codecov) ─────────────────────────────── -# Coverage is a tracked-but-informational metric: the target is 95%+ but the CI -# gate never turns red (baseline ~72%). See codecov.yml + AGENTS.md. -# Every recipe first checks that cargo-llvm-cov is installed. -LLVM_COV_INSTALL := Install with: cargo install cargo-llvm-cov --locked -LLVM_COV_HTML := $(TARGET_DIR)/llvm-cov/html/index.html +# Keep all supported local/CI entry points byte-for-byte equivalent in ordering. +# The version check is the first Cargo subprocess and every later Cargo command +# is locked. Recursive invocations make the ordering explicit even under make -j. +check: + @$(MAKE) --no-print-directory workspace-version + @$(MAKE) --no-print-directory tools-check + @$(MAKE) --no-print-directory fmt-check + @$(MAKE) --no-print-directory docs-check + @$(MAKE) --no-print-directory workflow-lint + @$(MAKE) --no-print-directory lint + @$(MAKE) --no-print-directory test + @$(MAKE) --no-print-directory release + @$(MAKE) --no-print-directory guardrail + @$(MAKE) --no-print-directory script-tests + @echo "All checks passed." + +ci: check +pre-ci: check archive-smoke + +archive-smoke: + bash scripts/smoke-release-archive.sh + +LLVM_COV_INSTALL := Install with: cargo install cargo-llvm-cov --version 0.8.7 --locked +LLVM_COV_HTML := $(TARGET_DIR)/llvm-cov/html/index.html define REQUIRE_LLVM_COV - @command -v cargo-llvm-cov >/dev/null 2>&1 || { \ - echo "❌ cargo-llvm-cov not found. $(LLVM_COV_INSTALL)"; exit 1; } + @command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "cargo-llvm-cov not found. $(LLVM_COV_INSTALL)"; exit 1; } endef -# Print a workspace coverage summary (line/region/function %) to stdout. coverage: $(REQUIRE_LLVM_COV) - @echo "📈 Measuring workspace coverage (summary)..." $(CARGO) llvm-cov --workspace --summary-only --ignore-filename-regex 'codegraph-bench' -# Generate the browsable HTML report under target/llvm-cov/html/. coverage-html: $(REQUIRE_LLVM_COV) - @echo "📈 Generating HTML coverage report..." $(CARGO) llvm-cov --workspace --html --ignore-filename-regex 'codegraph-bench' - @echo "✅ HTML report: $(LLVM_COV_HTML)" + @echo "HTML report: $(LLVM_COV_HTML)" -# Produce lcov.info (the artifact CI uploads to Codecov). coverage-lcov: $(REQUIRE_LLVM_COV) - @echo "📈 Generating lcov.info..." $(CARGO) llvm-cov --workspace --lcov --output-path lcov.info --ignore-filename-regex 'codegraph-bench' - @echo "✅ Wrote lcov.info" -# Build the HTML report and open it (best-effort; never fails if no opener). coverage-open: coverage-html - @if command -v xdg-open >/dev/null 2>&1; then \ - xdg-open "$(LLVM_COV_HTML)" >/dev/null 2>&1 || true; \ - elif command -v open >/dev/null 2>&1; then \ - open "$(LLVM_COV_HTML)" >/dev/null 2>&1 || true; \ - else \ - echo "ℹ️ No opener found; open manually: $(LLVM_COV_HTML)"; \ - fi - -# Clear coverage instrumentation/profraw data. + @if command -v xdg-open >/dev/null 2>&1; then xdg-open "$(LLVM_COV_HTML)" >/dev/null 2>&1 || true; \ + elif command -v open >/dev/null 2>&1; then open "$(LLVM_COV_HTML)" >/dev/null 2>&1 || true; \ + else echo "Open $(LLVM_COV_HTML) manually"; fi + coverage-clean: $(REQUIRE_LLVM_COV) - @echo "🧹 Cleaning coverage data..." $(CARGO) llvm-cov clean --workspace -# Enable the version-controlled pre-push hook (run once per clone). -# Points core.hooksPath at .githooks so `git push` runs the local quality gate -# (fmt + clippy + test + guardrail) before anything reaches GitHub. hooks setup-hooks: - @echo "🪝 Enabling version-controlled git hooks (core.hooksPath -> .githooks)..." git config core.hooksPath .githooks - @echo "✅ Done. The pre-push gate is now active (fmt + clippy + test + guardrail on push)." + @echo "Enabled .githooks (pre-push runs make pre-ci)." -# Clean build artifacts clean: - @echo "🧹 Cleaning build artifacts..." $(CARGO) clean @rm -rf $(DIST_DIR) - @echo "✅ Clean complete" -# Show debug vs release binary size size-compare: build-dev - @echo "" - @echo "📊 Binary size comparison:" - @echo "Debug:" - @ls -lh $(TARGET_DIR)/debug/$(BINARY_NAME) 2>/dev/null || echo " Not found" - @if [ -f "$(TARGET_DIR)/release/$(BINARY_NAME)" ]; then \ - echo "Release:"; \ - ls -lh $(TARGET_DIR)/release/$(BINARY_NAME); \ - fi + @ls -lh "$(TARGET_DIR)/debug/$(BINARY_NAME)" + @if [ -f "$(TARGET_DIR)/release/$(BINARY_NAME)" ]; then ls -lh "$(TARGET_DIR)/release/$(BINARY_NAME)"; fi help: - @echo "Available targets:" - @echo "" - @echo " Build:" - @echo " build - Build the CLI (debug) into dist/" - @echo " release - Build the optimized CLI (release) into dist/" - @echo " build-prod - Alias for release" - @echo " release-target TARGET= - Cross-compile for a target" - @echo " e.g. make release-target TARGET=x86_64-unknown-linux-musl" - @echo "" - @echo " Install:" - @echo " install - cargo install the CLI to ~/.cargo/bin" - @echo " uninstall - cargo uninstall the CLI" - @echo "" - @echo " Development:" - @echo " fmt - Format code (cargo fmt --all)" - @echo " fmt-check - Check formatting (CI)" - @echo " lint - Run clippy with -D warnings" - @echo " check - cargo check the workspace" - @echo " test - Run the full test suite" - @echo " guardrail - Run the scope guardrail (no AI/vector/LLM crates)" - @echo " ci - fmt-check + lint + test + guardrail" - @echo " hooks - Enable the pre-push git hook (run once after clone)" - @echo "" - @echo " Coverage (cargo-llvm-cov + Codecov; target 95%, informational gate):" - @echo " coverage - Workspace coverage summary to stdout" - @echo " coverage-html - HTML report -> target/llvm-cov/html/index.html" - @echo " coverage-lcov - Write lcov.info (uploaded to Codecov in CI)" - @echo " coverage-open - Build HTML report and open it (best-effort)" - @echo " coverage-clean - Clear coverage instrumentation data" - @echo "" - @echo " Utilities:" - @echo " clean - Remove build artifacts and dist/" - @echo " size-compare - Show debug vs release binary size" - @echo " help - Show this help" - @echo "" - @echo " Release profile (configured in Cargo.toml [profile.release]):" - @echo " opt-level = 3, lto = \"fat\", codegen-units = 1, strip = true" + @printf '%s\n' \ + 'Targets:' \ + ' check / ci complete quality gate (ci is an alias)' \ + ' pre-ci complete gate plus local archive smoke' \ + ' fmt / fmt-check Rust plus repository text formatting' \ + ' typecheck locked cargo check for the workspace' \ + ' lint / test / release locked Rust gates and shipped build' \ + ' workflow-lint actionlint plus shellcheck' \ + ' docs-check local Markdown links, mirrors, and source contracts' \ + ' workspace-version version/Cargo.lock consistency gate' \ + ' coverage[-html|-lcov] informational coverage reports' \ + ' archive-smoke package, unpack, and execute local release bytes' \ + ' hooks enable the versioned pre-push hook' diff --git a/README.md b/README.md index 1ae125d5..3c88d0f4 100644 --- a/README.md +++ b/README.md @@ -1,578 +1,290 @@ -# CodeGraph-Rust - -[![CI](https://github.com/sunerpy/codegraph-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/sunerpy/codegraph-rust/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/sunerpy/codegraph-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/sunerpy/codegraph-rust) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](#license) - -> A deterministic **code knowledge graph**: tree-sitter parsing persisted to -> SQLite/FTS5, exposing traversable symbol / call / dependency relationships to -> AI coding agents and developers. - -> 中文说明 / Chinese: [`docs/readme/README.zh-CN.md`](docs/readme/README.zh-CN.md) - -CodeGraph reads a codebase, extracts symbols and their relationships with -tree-sitter, persists them to a per-project SQLite database (FTS5 search -included), and exposes the result through a CLI and an MCP stdio server. -No AI/LLM anywhere inside the binary — output is byte-stable and deterministic. - ---- - -## Table of Contents - -- [Quickstart](#quickstart) -- [Installation](#installation) - - [One-liner install (scripts)](#one-liner-install-scripts) - - [Prebuilt binaries](#prebuilt-binaries) - - [With cargo (from git)](#with-cargo-from-git) -- [MCP Quick-Register](#mcp-quick-register) -- [Install the Agent Skill](#install-the-agent-skill-codegraph-skill) -- [Using CodeGraph in IDEs](#using-codegraph-in-ides) -- [CodeGraph for Zed (extension)](#codegraph-for-zed-extension) -- [Using CodeGraph with an LLM](#using-codegraph-with-an-llm) -- [Daemon, Watch & Configuration](#daemon-watch--configuration) -- [CLI Subcommands](#cli-subcommands) -- [Shell Completion](#shell-completion) -- [What CodeGraph Does (and Doesn't)](#what-codegraph-does-and-doesnt) -- [Supported Languages](#supported-languages) -- [Documentation](#documentation) -- [License](#license) - ---- +
-## Quickstart +# CodeGraph-Rust -**Install (Linux / macOS):** +**Deterministic code intelligence for agents and developers.** -```sh -curl -fsSL https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.sh | sh -``` +Tree-sitter extraction, SQLite/FTS5 search, graph traversal, CLI, and MCP in one +native binary. No AI or vector runtime inside the indexer. -**Install (Windows, PowerShell):** +[![CI](https://github.com/sunerpy/codegraph-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/sunerpy/codegraph-rust/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/sunerpy/codegraph-rust)](https://github.com/sunerpy/codegraph-rust/releases) +[![Codecov](https://codecov.io/gh/sunerpy/codegraph-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/sunerpy/codegraph-rust) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE-MIT) -```powershell -irm https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.ps1 | iex -``` +[English](README.md) · [简体中文](docs/readme/README.zh-CN.md) · +[Documentation](docs/README.md) · [Contributing](CONTRIBUTING.md) -**Index a project and search it:** +
-```bash -codegraph init /path/to/project # create .codegraph/ and run the first index -codegraph search "" -p /path/to/project # full-text symbol search (`query` is a legacy alias) -codegraph serve --mcp --path /path/to/project # MCP server (--path optional, defaults to cwd) -codegraph install --yes --init # wire detected agents + initialize cwd -``` +## Why CodeGraph -Project paths are intentionally parsed differently by command family: -lifecycle commands use an optional positional path (`codegraph status .`), -while research commands use `-p/--path` -(`codegraph search "" -p .`). If a command rejects an argument, check -`codegraph --help` before falling back to text search. +CodeGraph turns a source tree into a local knowledge graph: symbols become nodes; +calls, imports, inheritance, ownership, references, and type relationships become +edges. The graph is persisted per project and can be queried without asking an +LLM to rediscover structure through repeated text searches. ---- +- **Deterministic:** no model calls, embeddings, or vector search; canonical graph + output is guarded by byte-stable golden fixtures. +- **Source-aware:** search, callers/callees, impact, file source, and multi-file + exploration use one indexed representation. +- **Agent-ready:** an MCP server exposes the same graph and verbatim source used by + the CLI. +- **Local-first:** the index lives under the project; the shared daemon and HTTP + transport are local processes. +- **Broad language coverage:** grammar-backed languages, embedded/template files, + and framework-aware Godot/Tauri/JS ecosystem relationships share one schema. +- **Incremental:** `sync` and the watcher update changed files while preserving the + same final canonical result as a clean index. -## Installation +Exact language and static-analysis boundaries are documented in +[`docs/languages.md`](docs/languages.md). -The CLI package is **`codegraph-rs`** — a single binary named `codegraph`. SQLite -is compiled in (no system SQLite needed). Not published to crates.io. +## Install -
-🤖 For AI agents / LLMs — read this first +### Verified release installer -You are an AI coding agent. CodeGraph is a deterministic code knowledge graph -(tree-sitter + SQLite/FTS5) that answers structural questions about a codebase — -"who calls X", "what does changing X break", "where is X", "how does this area -work" — in **one sub-millisecond query instead of dozens of grep + file reads**. -It returns verbatim source plus the caller/callee/impact graph, so you get more -accurate context in far fewer tokens and round-trips. No AI/LLM inside it — it is -pure pre-computed structure for _you_ to consume. +The installers select the platform archive, require its entry in `SHA256SUMS`, +verify the checksum, and then install the binary. -**Install + index a project** — prefer the one-line installer (downloads a -prebuilt binary; no Rust toolchain, no compile wait): - -```bash -# Linux / macOS — one-click install (recommended) +```sh +# Linux / macOS curl -fsSL https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.sh | sh -# Windows (PowerShell 5.1+) -irm https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.ps1 | iex - -# Fallback — build from source (only if you have a Rust toolchain) -cargo install --git https://github.com/sunerpy/codegraph-rust codegraph-rs # binary: `codegraph` - -codegraph status /path/to/project --json # inspect index health first -codegraph init /path/to/project # create the first usable index -codegraph sync /path/to/project # ordinary added/changed/deleted files -codegraph index /path/to/project # intentional full rebuild -codegraph index --force /path/to/project # only when diagnostics prescribe it ``` -> 中文:优先用上面的一键安装脚本(直接下载预编译二进制,无需 Rust 工具链、无需编译等待); -> `cargo install --git` 仅作为有 Rust 环境时的回退方案。 - -**Use it as an MCP server (recommended for agents).** It speaks MCP over stdio: - -```bash -codegraph serve --mcp # defaults to cwd (recommended: use codegraph install) -codegraph serve --mcp --path /path/to/project # optional: pin to a specific project -``` - -Auto-register it into your agent's config (Claude Code, Cursor, Codex CLI, -opencode, Hermes, Gemini CLI, Antigravity, Kiro, Trae, Qoder, Zed, Zuno, -VS Code, Copilot CLI, JetBrains): - -```bash -codegraph install --yes # detects installed agents and wires them up -codegraph install --yes --init # one-shot unattended install + cwd index +```powershell +# Windows PowerShell 5.1+ +irm https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.ps1 | iex ``` -**MCP tools you can call** (prefer these over grep/read for indexed source): - -| tool | use it for | -| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `codegraph_explore` | PRIMARY — "how does X work", architecture, a flow, surveying an area. One call returns the relevant symbols' source grouped by file. | -| `codegraph_search` | locate a symbol by name (kind + file:line + signature) | -| `codegraph_node` | read a symbol/file's verbatim source + its caller/callee trail (a smarter `Read`) | -| `codegraph_callers` / `codegraph_callees` | who calls this / what this calls | -| `codegraph_impact` | blast radius of changing a symbol (transitive) | -| `codegraph_files` / `codegraph_status` | list a directory / check index readiness | - -**Rules of thumb:** reach for `codegraph_explore` _before_ reading files; trust -its results (full AST parse — don't re-verify with grep); use `codegraph_impact` -for refactor blast-radius rather than walking callers by hand. The index lags -file writes by ~1s; tool responses flag any stale files. If the question names -a source path, include it in the explore query: explicit paths are normalized -and pinned ahead of fuzzy matches, while unresolved paths are reported. - -
- -### One-liner install (scripts) - -The quickest path — the scripts detect your platform, download the right binary, -and put it on your PATH: +Pin an exact release when reproducibility matters: ```sh -# Linux / macOS -curl -fsSL https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.sh | sh - -# Windows (PowerShell 5.1+) -irm https://raw.githubusercontent.com/sunerpy/codegraph-rust/main/scripts/install.ps1 | iex +curl -fsSL https://raw.githubusercontent.com/sunerpy/codegraph-rust/vX.Y.Z/scripts/install.sh \ + | CODEGRAPH_VERSION=vX.Y.Z sh ``` -Set `CODEGRAPH_VERSION=v0.3.0` to pin a specific release instead of the latest. - -### Prebuilt binaries +```powershell +$env:CODEGRAPH_VERSION = "vX.Y.Z" +irm https://raw.githubusercontent.com/sunerpy/codegraph-rust/vX.Y.Z/scripts/install.ps1 | iex +``` -Download an archive from the -[Releases](https://github.com/sunerpy/codegraph-rust/releases) page, extract it, -and place `codegraph` on your PATH. Assets are named -`codegraph--.`: +### Prebuilt archives -| Platform | Arch | Target | Ext | -| -------- | ----------------------- | ---------------------------- | ------- | -| Linux | x86_64 (static, musl) | `x86_64-unknown-linux-musl` | .tar.gz | -| Linux | aarch64 (static, musl) | `aarch64-unknown-linux-musl` | .tar.gz | -| macOS | x86_64 | `x86_64-apple-darwin` | .tar.gz | -| macOS | aarch64 (Apple Silicon) | `aarch64-apple-darwin` | .tar.gz | -| Windows | x86_64 | `x86_64-pc-windows-msvc` | .zip | -| Windows | aarch64 (ARM64) | `aarch64-pc-windows-msvc` | .zip | +GitHub Releases publish these archive families: -Linux builds are statically linked (musl) — no glibc or SQLite system dependency. +| Platform | Target | Archive | +| ------------------- | ---------------------------- | --------- | +| Linux x86_64 | `x86_64-unknown-linux-musl` | `.tar.gz` | +| Linux ARM64 | `aarch64-unknown-linux-musl` | `.tar.gz` | +| macOS Intel | `x86_64-apple-darwin` | `.tar.gz` | +| macOS Apple Silicon | `aarch64-apple-darwin` | `.tar.gz` | +| Windows x86_64 | `x86_64-pc-windows-msvc` | `.zip` | +| Windows ARM64 | `aarch64-pc-windows-msvc` | `.zip` | -### With cargo (from git) +Each release also includes `SHA256SUMS`. GitHub CLI users can additionally verify +an archive's build provenance: ```bash -cargo install --git https://github.com/sunerpy/codegraph-rust codegraph-rs +gh attestation verify codegraph-X.Y.Z-x86_64-unknown-linux-musl.tar.gz \ + --repo sunerpy/codegraph-rust \ + --signer-workflow sunerpy/codegraph-rust/.github/workflows/release.yml \ + --deny-self-hosted-runners ``` -For a full source build (optimized binary + developer targets), see -[`docs/architecture.md`](docs/architecture.md) or run `make help`. - ---- +### Build from Git -## MCP Quick-Register +The project is not published to crates.io: -Add to your agent's MCP config (or run `codegraph install --yes` to do it -automatically): - -```jsonc -{ - "mcpServers": { - "codegraph": { - "command": "codegraph", - "args": ["serve", "--mcp"], - }, - }, -} +```bash +cargo install --locked --git https://github.com/sunerpy/codegraph-rust codegraph-rs ``` -**Default (no `-p`):** one config works for all your projects — each just needs -an index first. The stdio server finds an indexed ancestor; from an unindexed -workspace root (`.git` or a workspace manifest), it also adopts exactly one -indexed child project. Multiple candidates are never guessed and are listed in -stderr/tool guidance; pass `projectPath` per call or pin `--path`. Client -`rootUri`/`workspaceFolders`/`roots` remain another adoption path. When no -default resolves, `projectPath` is marked required per tool call. -**Optional `-p `:** pin to one fixed project -(e.g. `"args": ["serve", "--mcp", "-p", "/abs/path/to/project"]`). +The installed executable is `codegraph`; SQLite is bundled. -See [`docs/mcp.md`](docs/mcp.md#project-resolution) for the full three-case breakdown. +## Quickstart -Supported agents: Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, -Gemini CLI, Antigravity IDE, Kiro, Trae, Qoder, Zed, Zuno, VS Code -(GitHub Copilot), GitHub Copilot CLI, JetBrains IDEs (GitHub Copilot). +Create an index, check its state, then ask a structural question: ```bash -codegraph install --yes # auto-detect installed agents -codegraph install --yes --init # wire agents, then initialize cwd -codegraph install --target=claude,cursor --yes # explicit list -codegraph install --target=auto --local # project-local config -codegraph install --target=codex --local --yes # .codex/config.toml + AGENTS.md + local skill +cd /path/to/project +codegraph init . +codegraph status . --json +codegraph search "main" -p . +codegraph explore "startup and configuration flow" -p . ``` -Codex project-local configuration is applied only after the repository is -trusted in Codex. `install` never indexes implicitly; only explicit `--init` -does so, and `--print-config` always returns without writing or indexing. - -Full MCP tool reference: [`docs/mcp.md`](docs/mcp.md). - ---- - -## Install the Agent Skill (`codegraph skill`) - -Beyond wiring the MCP server, CodeGraph can install a `SKILL.md` directly into -each agent's skill directory. The skill teaches your agent to use CodeGraph for -code research and project onboarding — reach for `codegraph_explore` before -grep/read, use `codegraph_node` instead of a plain file read on indexed source, -inspect `codegraph status`, run `codegraph init` when no usable index exists, -and use `codegraph sync` for ordinary manual catch-up. +Lifecycle commands take a positional project path. Research commands take one +query or target plus `-p/--path`. For example: ```bash -codegraph skill install --yes # install into all detected agents (global) -codegraph skill install --target=claude,cursor --yes # explicit list -codegraph skill install --target=auto --local # project-local skill dirs -codegraph skill update # show version/+/- summary, then refresh -codegraph skill update --diff # also print the unified content diff -codegraph skill update --dry-run --diff # preview without writing files -codegraph skill update --force # overwrite even locally-modified files -codegraph skill uninstall --target=claude --yes # remove from one agent -codegraph skill status # show state for all detected agents +codegraph sync . +codegraph node "ReferenceResolver" -p . +codegraph callers "ReferenceResolver" -p . +codegraph impact "ReferenceResolver" -p . ``` -Eleven of the fifteen install targets have a skill directory (Claude Code, -Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, -Trae, Qoder, and Zuno). Zuno reuses the standard `~/.agents/skills` / -`.agents/skills` path, so selecting both Codex and Zuno is idempotent. Zed and -the three GitHub Copilot surfaces are MCP-only and have no skill directory. -Default location is `--global`; pass `--local` to write into the project tree. -Hermes supports global only. - -**Update behavior.** `skill update` compares the installed file's content hash -against the embedded version. An unmodified file is refreshed automatically; a -hand-edited file is skipped with a "locally modified" note (pass `--force` to -overwrite). A small sidecar file (`.codegraph-skill.json`) records the installed -hash so the update check can distinguish "outdated" from "locally modified". -Before writing, update prints the installed-to-embedded version transition and -added/removed line counts. `--diff` adds a deterministic unified diff; -`--dry-run` prints the same preview without changing files. `skill status` also -shows version provenance, for example `outdated (0.40.1 -> 0.47.0)`. - -For agents whose installer owns a marker-fenced instructions block, `skill -update` also refreshes that block while preserving all user text outside -`` / ``. This includes Zuno's -global `$XDG_CONFIG_HOME/zuno/AGENTS.md` (normally -`~/.config/zuno/AGENTS.md`) and project-root `AGENTS.md`. `--dry-run` previews -the instructions change without writing it. - -Full reference including per-agent skill paths: [`docs/cli.md`](docs/cli.md). - ---- - -## Using CodeGraph in IDEs - -`codegraph install` registers the MCP server entry for each supported agent/IDE. -Run it once and every indexed project is immediately available. How well the index -stays live depends on whether the IDE expands `${workspaceFolder}`: - -- **Cursor / Trae** — global config uses `--path ${workspaceFolder}`, so one entry auto-follows every project window. Live watch enabled. -- **Kiro / Qoder** — global entry without `--path`; tools work read-only off the existing index. Run `codegraph init --target=kiro` (or `--target=qoder`) inside each project for live watch. -- **Zed** — global `settings.json` entry without `--path`. Run `codegraph init --target=zed` inside each project to write a `.zed/settings.json` with an absolute `--path` — the only way to give Zed a per-project path. The installer also writes `//`-commented HTTP and SSH alternatives for remote-development use. -- **Zuno** — global config is `$XDG_CONFIG_HOME/zuno/zuno.json[c]` (normally `~/.config/zuno/zuno.json[c]`); project config is `.zuno/zuno.json[c]`. The installer writes `mcp.codegraph` and migrates the older `mcp.codegraph-mcp-server` key without disturbing siblings or JSONC comments. -- **Codex CLI** — global install keeps `~/.codex/config.toml` + `~/.codex/AGENTS.md`; local install writes `.codex/config.toml`, project-root `AGENTS.md`, and `.agents/skills/codegraph`. Trust the project in Codex to activate its local layer. -- **Other agents** (Claude Code, opencode, Hermes, Gemini CLI, Antigravity) — their native MCP entry shape; live watch where the daemon can reach the project. - -> **Zed Remote (SSH).** Zed runs MCP `context_servers` on the local client, not on -> the remote host. If codegraph tools return empty in a remote SSH session, use the -> `//`-commented SSH bridge or HTTP alternative the installer wrote into your -> `settings.json`. HTTP (`codegraph serve --http`, then `http://localhost:8111/mcp`) -> is the recommended remote transport. See -> [`docs/mcp.md` — Zed over SSH](docs/mcp.md#zed-over-ssh-remote-development). - -Full per-IDE config details and the `${workspaceFolder}` matrix: [`docs/mcp.md`](docs/mcp.md). - ---- - -## CodeGraph for Zed (extension) +If a command rejects an argument, run `codegraph --help`; do not abandon +an available index because a lifecycle and research command use different path +syntax. -A standalone Zed extension lives under [`editors/zed/`](editors/zed/). It -registers CodeGraph as a `context_servers` context server inside Zed and -downloads the right platform binary automatically — no separate install step -needed. +## CLI -### Install +Common workflows: -**Preferred — official registry:** - -Search for **"CodeGraph"** in Zed's extension registry (`zed: extensions` from the -command palette) and click Install. It is listed as **CodeGraph MCP Server** -(extension ID `codegraph-mcp`). The extension auto-downloads the CodeGraph -binary for your platform on first launch. - -**Dev install (for local development):** - -1. Clone this repository. -2. In Zed, open the command palette and run **`zed: install dev extension`**. -3. Select the `editors/zed/` directory. - -Zed compiles the extension to WebAssembly and registers a `codegraph` context -server. On first launch it downloads the latest CodeGraph release binary for your -platform. - -### Auto-update and binary cache location - -The extension never pins a CodeGraph version. On each launch it resolves the -**latest** `sunerpy/codegraph-rust` GitHub release, picks the asset matching your -platform, downloads and extracts it, then caches the binary at: - -``` -codegraph-/codegraph # Linux / macOS -codegraph-/codegraph.exe # Windows -``` - -This path is **relative to the extension's working directory** that Zed manages, -which is keyed on the extension ID: typically -`~/.local/share/zed/extensions/work/codegraph-mcp/` on Linux, -`~/Library/Application Support/Zed/extensions/work/codegraph-mcp/` on -macOS, or `%LOCALAPPDATA%\Zed\extensions\work\codegraph-mcp\` on Windows. - -For example, after downloading version `v0.25.0` on Linux the binary lives at: - -``` -~/.local/share/zed/extensions/work/codegraph-mcp/codegraph-v0.25.0/codegraph +```bash +# Index lifecycle +codegraph status . --json +codegraph sync . +codegraph index . # full rebuild of the selected project + +# Research +codegraph search "symbol" -p . +codegraph explore "area or flow" -p . +codegraph node "symbol-or-id" -p . +codegraph files -p . --format tree + +# Relationships +codegraph callers "symbol" -p . +codegraph callees "symbol" -p . +codegraph impact "symbol" -p . + +# Operations +codegraph serve --mcp --path . +codegraph serve --http --path . +codegraph mcp list +codegraph http list ``` -When the CodeGraph CLI ships a new release the extension picks up the new binary -automatically on the next launch — **no extension re-publish or manual update -required**. If the GitHub API is unreachable, the extension falls back to the -binary it has already cached. - -Right after each successful download the extension removes the other -`codegraph-*/` directories in that working directory, so the cache keeps exactly -one binary instead of one per release. A launch that hits the cache does no -cleanup, the in-use version is always kept, and a directory that cannot be -deleted (an in-use `codegraph.exe` on Windows) never fails the launch. See -[`editors/zed/README.md`](editors/zed/README.md) for the full cleanup contract. +`query` remains an alias for `search`. For every command and flag, see +[`docs/cli.md`](docs/cli.md). -### Override with your own binary +## MCP -If you already have `codegraph` installed via the CLI, or want to pin a specific -project path, add a `command` override in your project's `.zed/settings.json`. -The extension uses it verbatim and skips the download: +Register the stdio server manually: ```jsonc { - "context_servers": { + "mcpServers": { "codegraph": { - "command": { - "path": "codegraph", - "args": ["serve", "--mcp", "--path", "/abs/path/to/project"], - "env": {}, - }, + "command": "codegraph", + "args": ["serve", "--mcp"], }, }, } ``` -Or let the installer write it for you: +Or let the installer update supported agent configurations: ```bash -cd /your/project -codegraph init --target=zed # writes .zed/settings.json with absolute --path +codegraph install --yes +codegraph install --yes --init +codegraph install --target=codex,claude,kiro --yes ``` -See [`editors/zed/README.md`](editors/zed/README.md) for the full extension -reference, and [`docs/mcp.md`](docs/mcp.md#zed----context_servers-config) for the -Zed `context_servers` config shape. - ---- +A server launched without `--path` can serve an existing index selected by an +explicit per-call `projectPath`, client roots, or deterministic workspace +adoption. Pin `--path` when a project should be the default rather than +supplied per call. -## Using CodeGraph with an LLM +The default visible MCP surface emphasizes exploration, file/symbol reads, +search, and callers. Additional known tools can be enabled with +`CODEGRAPH_MCP_TOOLS`. Tool schemas, project resolution, stdio/HTTP behavior, +and protocol compatibility are in [`docs/mcp.md`](docs/mcp.md). -CodeGraph has **no built-in LLM**, but it's designed to feed one. The division of -labor: CodeGraph produces deterministic structural facts (call graph, blast radius, -centrality) at sub-millisecond speed; your LLM reasons over the already-located -small context. +## Agents and IDEs -Two patterns: **MCP** (agent calls codegraph tools directly) or **backend -orchestration** (your service calls `export`/`explore`, assembles a prompt, feeds -your LLM). A runnable example: +`codegraph install` supports common coding agents and IDE integrations. Some +clients can expand a workspace variable in global configuration; others require a +project-local absolute path for live watching. The installer prints the relevant +caveat rather than guessing. ```bash -python examples/llm_orchestration.py --repo . --query "how does indexing work" +codegraph install --target=auto --global --yes +codegraph install --target=auto --local --yes +codegraph init --target=kiro . +codegraph init --target=zed . ``` -See [`examples/llm_orchestration.py`](examples/llm_orchestration.py) for the full -script. Neither pattern touches the no-AI guardrail — that only bars embedding an -LLM library _inside_ the codegraph binary itself. - ---- - -## Daemon, Watch & Configuration - -CodeGraph spawns a shared background daemon for each indexed project when you run -`codegraph serve --mcp`. Multiple MCP clients (terminal tabs, agents) share that -one daemon via a Unix socket (`.codegraph/daemon.sock`). It exits once all -clients disconnect and the idle timeout elapses. - -Key operations: +The optional embedded skill teaches an agent to use CodeGraph before grep/read: ```bash -codegraph unlock [path] # clear a stale daemon lock (keeps live pids) -codegraph serve --http # HTTP MCP transport, binds 127.0.0.1:8111 by default -codegraph http list # table of running HTTP servers -codegraph http stop # terminate one HTTP server by address +codegraph skill install --yes +codegraph skill status +codegraph skill update --dry-run --diff ``` -Set `CODEGRAPH_NO_DAEMON=1` to force foreground mode (useful in CI). The daemon -watches files with a 2 s debounce; pass `--no-watch` or set `CODEGRAPH_NO_WATCH=1` -to disable. Custom extension mapping goes in `.codegraph/codegraph.json`; -exclude patterns in `.codegraph/config.toml` under `[indexing] exclude`. -Changes to those two files, or to the project-root `.gitignore`, hot-reload the -running watcher and trigger one full reconcile. Newly excluded files are removed -and newly included/custom-extension files are added without restarting the MCP -server. Invalid TOML keeps the last valid watcher scope and reports the error; -malformed JSON keeps the existing tolerant empty-override behavior. - -Keep third-party or generated source searchable while ranking it below -first-party code with ranking-only rules: - -```toml -[indexing] -deprioritize = ["vendor/**", "generated/**"] -``` - -`.codegraph/codegraph.json` also accepts a top-level `deprioritize` array for -compatibility. JSON rules run first, TOML rules run last, ordered `!pattern` -exceptions make matching last-rule-wins, and no file/node/edge is removed. -Search and explore reload the addressed project's policy on every request. - -Full env-var table, HTTP server details, filesystem fallback behavior, and the -Claude prompt-hook: [`docs/mcp.md`](docs/mcp.md) and -[`docs/cli.md`](docs/cli.md#daemon-watch--environment-variables). - ---- - -## CLI Subcommands - -Core commands: `init`, `index`, `sync`, `search`, `explore`, `node`, `files`, -`status`, `serve`, `callers`, `callees`, `impact`, `affected`, `check`, `audit`, -`export`, `unlock`, `http`, `mcp`. - -Agent / install commands: `install`, `uninstall`, `skill`, `self-update`, -`completions` (`--install` sets up tab completion for bash/zsh/fish/powershell/elvish). - -Path convention: most traversal commands accept the project path as a positional -argument or `-p/--path`; `search`/`files`/`serve` use `-p/--path`. -`query` remains a backward-compatible alias for `search`. - -> **Full reference with all flags:** [`docs/cli.md`](docs/cli.md) - ---- - -## Shell Completion +
+Compact guidance for coding agents -```bash -codegraph completions bash --install # Bash -codegraph completions zsh --install # Zsh -codegraph completions fish --install # Fish -codegraph completions powershell --install # PowerShell -codegraph completions elvish --install # Elvish -``` +1. Run `codegraph status --json` before research. +2. Use `codegraph_explore` first for architecture, a bug, or a flow. +3. Use `codegraph_search` to locate one name and `codegraph_node` to read one + symbol or indexed file with its caller/callee trail. +4. Use `codegraph_impact` before changing a shared symbol. +5. Trust the structural index; re-read only files explicitly reported stale. +6. Use `codegraph sync ` for ordinary catch-up. Rebuild only when status + or a requested operation requires it. -Omit `--install` to print to stdout. Full per-shell install paths and notes: -[`docs/cli.md`](docs/cli.md). +
---- +Full target and configuration matrices: +[`docs/cli.md`](docs/cli.md), [`docs/mcp.md`](docs/mcp.md), and +[`editors/zed/README.md`](editors/zed/README.md). -## What CodeGraph Does (and Doesn't) +## Determinism and safety -**Does:** deterministic code-structure extraction across 38 languages (TypeScript, -Python, Go, Rust, Java, C/C++, C#, Vue, Svelte, GDScript, and more — see -[`docs/languages.md`](docs/languages.md)), cross-file resolution (including -Godot project graphs), graph traversal, FTS5 search, whole-graph export with -deterministic PageRank centrality, MCP/CLI surfaces, and golden byte-stable -output. +The compatibility contract includes stable node IDs, canonical golden artifacts, +SQLite schema parity, deterministic resolution and ordering, project-contained +filesystem access, and fail-closed ambiguity. Incremental output is tested against +a clean full index. -Extraction is fail-closed per file: a source tree deeper than the deterministic -256-level logical named-node limit records a stable file error, emits no partial -graph for that file, and does not stop other files from being indexed. Incremental -`sync` removes any older graph for the failed file and converges with -`index --force`. +CodeGraph reports static evidence, not runtime certainty. Reflection, registries, +event buses, framework conventions, generated code, and dynamic dispatch can +continue beyond a known edge. Absence of a static reference is not proof that code +is dead. -Graph correctness includes Erlang `module::function/arity` identity, Rust -generic/reference/qualified `impl` ownership and validated -`self.field.method()` calls, JavaScript-family object-literal namespace members, -inherited TypeScript/JavaScript path aliases, Python import aliases, Lua -function-expression callables, extensionless `.xsjs`/`.xsjslib` imports, and -plain derived C++ classes/structs in `.h` files. See -[`docs/languages.md`](docs/languages.md) for the exact static-analysis -boundaries. +For schema and oracle details, read [`docs/data-model.md`](docs/data-model.md) and +[`docs/equivalence.md`](docs/equivalence.md). Security reports should follow +[`SECURITY.md`](SECURITY.md). -**Doesn't:** no AI / vector / embedding / LLM anywhere inside the binary (hard -constraint, `scripts/guardrail.sh`-enforced); no semantic search; no languages -beyond the fixed `LANGUAGES` set. +## Performance ---- +Performance is measured by the committed benchmark harness on pinned corpora. A +valid result records the implementation commits, environment, cache policy, run +count, median, dispersion, and query percentiles. This README intentionally makes +no floating latency claim. -## Supported Languages +Methodology and the current result status are in +[`docs/benchmark.md`](docs/benchmark.md) and +[`docs/benchmark-results.md`](docs/benchmark-results.md). -CodeGraph supports **38 languages** grouped by extraction depth. Quick overview: +## Development -- **Tier 1 — Full symbol extraction (29):** TypeScript, TSX, JavaScript, JSX, ArkTS, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Scala, Lua, Luau, Objective-C, R, Solidity, Nix, Terraform, Erlang, CFML, GDScript, Pascal. -- **Tier 2 — Embedded / template extraction (6):** Vue, Svelte, Astro, Razor/`.cshtml`, Liquid, XML/MyBatis mapper. -- **Tier 3 — File-level only (3):** YAML, Twig, Properties. +The pinned Rust toolchain and locked dependency graph are repository contracts. +Start with: -Full list with extensions and per-language notes: [`docs/languages.md`](docs/languages.md). +```bash +git clone https://github.com/sunerpy/codegraph-rust.git +cd codegraph-rust +make hooks +make check +``` ---- +`make check` and `make ci` run the same complete local quality path; `make pre-ci` adds a local package/unpack/execute smoke. Focused targets are available through `make help`. Contributors should read +[`CONTRIBUTING.md`](CONTRIBUTING.md) and the canonical agent contract in +[`AGENTS.md`](AGENTS.md). ## Documentation -- [`docs/architecture.md`](docs/architecture.md) — crate dependency graph, - extraction/resolution/traversal/search pipelines, daemon/watch lifecycle. -- [`docs/data-model.md`](docs/data-model.md) — SQLite/FTS5 storage contract. -- [`docs/equivalence.md`](docs/equivalence.md) — the 3-tier equivalence oracle, - golden regeneration, `KNOWN_DIFFS.md` format. -- [`docs/languages.md`](docs/languages.md) — full supported-language list grouped - by extraction depth. -- [`docs/godot.md`](docs/godot.md) — Godot static analysis: what CodeGraph - extracts from `.tscn`/`.tres`/`project.godot`/`.gd`, the static-vs-runtime - boundary, and honesty signals for dynamic reachability. -- [`docs/godot-gdext-decision.md`](docs/godot-gdext-decision.md) — why `gdext` - (godot-rust GDExtension bindings) is rejected for Godot analysis, and the - engine-free alternatives. -- [`docs/grammar-manifest.md`](docs/grammar-manifest.md) / - [`docs/embedded-extraction.md`](docs/embedded-extraction.md) — language support - and extraction tiers (engineering ABI detail). -- [`docs/cli.md`](docs/cli.md) — full CLI subcommand reference (22 subcommands, - all flags). -- [`docs/mcp.md`](docs/mcp.md) — MCP server protocol, all 10 tools, JSON-RPC - details, IDE per-agent config matrix, HTTP server, env-var reference. -- [`docs/troubleshooting.md`](docs/troubleshooting.md) — opt-in JSONL diagnostics - for slow `init` / `index` / `sync` runs and the feedback checklist. -- [`examples/`](examples/) — codegraph + LLM orchestration example. -- [`editors/zed/README.md`](editors/zed/README.md) — Zed extension reference. -- [`docs/readme/README.zh-CN.md`](docs/readme/README.zh-CN.md) — 中文说明. - ---- +- [`docs/README.md`](docs/README.md) — documentation map +- [`docs/architecture.md`](docs/architecture.md) — workspace and runtime design +- [`docs/cli.md`](docs/cli.md) — complete command reference +- [`docs/mcp.md`](docs/mcp.md) — MCP transports, tools, and clients +- [`docs/languages.md`](docs/languages.md) — language coverage and boundaries +- [`docs/equivalence.md`](docs/equivalence.md) — deterministic golden contract +- [`docs/upstream-sync/UPSTREAM.md`](docs/upstream-sync/UPSTREAM.md) — upstream ledger +- [`docs/troubleshooting.md`](docs/troubleshooting.md) — diagnostic workflow ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ab2d10a2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security Policy + +## Supported versions + +Security fixes are applied to the current `main` branch and the newest published +release. Older releases do not receive guaranteed backports. Upgrade to the +newest verified release before reporting an issue already fixed on `main`. + +## Report a vulnerability privately + +Do **not** open a public issue for a vulnerability or include exploit details in +a public discussion. Use GitHub Private Vulnerability Reporting: + + + +Include, when available: + +- affected version, commit, platform, and installation method; +- minimal reproduction and expected/observed behavior; +- impact and required attacker capabilities; +- whether the issue crosses a project-root, filesystem, process, MCP/HTTP, agent + configuration, archive/checksum, or release boundary; +- logs with secrets and personal paths removed; +- a proposed fix or regression test, if you have one. + +You should receive an acknowledgement within seven days. Disclosure timing will +be coordinated after the report is reproduced and a fix/release path exists. +Please do not publish details before that coordination completes. + +## Security boundaries + +CodeGraph performs static analysis and reads source in the selected project. Its +index is not a sandbox and should be protected like derived source code. MCP +clients and local HTTP users receive source excerpts and graph data according to +the project path they request. + +The project aims to keep managed paths within the selected project, reject +ambiguous resolution, keep MCP stdout protocol-clean, verify release checksums, +and fail closed on unsafe stale source or inconsistent state. These properties do +not make untrusted repositories safe to execute: do not run project scripts, +build hooks, or generated binaries merely to index source. + +For ordinary bugs and hardening ideas with no confidential exploit details, use +the public issue templates. diff --git a/audit.toml b/audit.toml deleted file mode 100644 index c0ec0dae..00000000 --- a/audit.toml +++ /dev/null @@ -1,32 +0,0 @@ -# cargo-audit configuration. -# -# NOTE ON LOCATION: cargo-audit reads `.cargo/audit.toml` (relative to the working -# directory) or `~/.cargo/audit.toml` — it does NOT read this repo-root -# `audit.toml`. CI runs bare `cargo audit` from the repo root, so the file it -# actually honors is `.cargo/audit.toml`. This repo-root copy is kept as the -# human-facing reference and MUST be kept in sync with `.cargo/audit.toml`; the -# `[advisories] ignore` list below mirrors it exactly. -# -# RUSTSEC-2025-0119: `number_prefix` was an unmaintained transitive dependency -# (codegraph-rs -> self_update -> indicatif -> number_prefix). The indicatif 0.17 -# -> 0.18 bump replaced it with `unit-prefix`, so number_prefix is no longer in -# the dependency tree; this ignore is now inert and can be dropped on the next -# audit.toml cleanup. Kept for now to stay conservative. -# -# RUSTSEC-2026-0194 / RUSTSEC-2026-0195: two DoS advisories in `quick-xml` -# (quadratic runtime on duplicate start-tag attribute names; unbounded -# namespace-declaration allocation in `NsReader`). Both are patched only in -# quick-xml >= 0.41.0. quick-xml is a transitive dependency ONLY via -# `self_update -> quick-xml`, and NO published `self_update` release depends on -# quick-xml >= 0.41 (0.44 and 1.0.0-rc.1 both pin `^0.38`), so cargo cannot -# resolve quick-xml to a patched version without breaking self_update. We already -# bumped self_update 0.42 -> 0.44 to move quick-xml off the 0.37 line to 0.38.4. -# Reachability: quick-xml is used solely by self_update's GitHub backend -# (cmd_self_update in crates/codegraph-cli/src/main.rs -> self_update::backends::github::Update -# / get_latest_release) to parse GitHub Release METADATA fetched over TLS from the -# trusted github.com API. CodeGraph never feeds untrusted or adversarial XML to -# quick-xml, so neither DoS is reachable in this project's threat model. -# Documented risk acceptance; DROP these two ignores once a self_update release -# depends on quick-xml >= 0.41. -[advisories] -ignore = ["RUSTSEC-2025-0119", "RUSTSEC-2026-0194", "RUSTSEC-2026-0195"] diff --git a/changelog/README.md b/changelog/README.md index 9f1eb473..02497831 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -9,6 +9,8 @@ This `changelog/` directory is the single source of truth for the project's chan **How updates work:** - The active changelog file (e.g., `CHANGELOG-v0.x.md`) is maintained automatically by release-please in its release PR. The [`release-please-config.json`](../release-please-config.json) sets `"changelog-path": "changelog/CHANGELOG-v0.x.md"`. When a new major series begins, a new `CHANGELOG-vN.x.md` is created and `changelog-path` is bumped. -- GitHub Release notes (shown on the Releases page for each tag) are rendered separately by [git-cliff](https://git-cliff.org) (config: [`cliff.toml`](../cliff.toml)) in the release workflow; that is distinct from the repo's changelog file. +- Release Please uses the same generated changelog section as the draft GitHub + Release body. The release workflow verifies and publishes that body unchanged; + there is no second release-notes generator. **Do not hand-edit:** These files are auto-generated and excluded from `oxfmt` (see `.oxfmtignore`). diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index 2368f74b..00000000 --- a/cliff.toml +++ /dev/null @@ -1,83 +0,0 @@ -# git-cliff configuration -# https://git-cliff.org/docs/configuration - -[changelog] -header = """ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -""" - -body = """ -{% if version %}\ - ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ - ## [Unreleased] -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - - ### {{ group | striptags | trim | upper_first }} - - {% for commit in commits %}\ - - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\ - {% if commit.breaking %}[**BREAKING**] {% endif %}\ - {{ commit.message | upper_first | trim }}\ - {% if commit.remote.pr_number %} \ - ([#{{ commit.remote.pr_number }}](https://github.com/sunerpy/codegraph-rust/pull/{{ commit.remote.pr_number }}))\ - {% endif %} - {% if commit.body %}\ -{{ commit.body | trim | indent(prefix=" ") }} - {% endif %}\ - {% endfor %} -{% endfor %} -""" - -footer = """ ---- -*Generated by [git-cliff](https://github.com/orhun/git-cliff)* -""" - -trim = true - -postprocessors = [{ pattern = '', replace = "https://github.com/sunerpy/codegraph-rust" }] - -[git] -conventional_commits = true -filter_unconventional = true -split_commits = false - -commit_preprocessors = [ - { pattern = '\(#([0-9]+)\)', replace = "([#${1}](https://github.com/sunerpy/codegraph-rust/issues/${1}))" }, - { pattern = ' +', replace = " " }, -] - -commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix", group = "Bug Fixes" }, - { message = "^doc", group = "Documentation" }, - { message = "^docs", group = "Documentation" }, - { message = "^perf", group = "Performance" }, - { message = "^refactor", group = "Refactor" }, - { message = "^style", group = "Styling" }, - { message = "^test", group = "Testing" }, - { message = "^build", group = "Build" }, - { message = "^ci", group = "CI/CD" }, - { message = "^chore\\(release\\)", skip = true }, - { message = "^chore\\(changelog\\)", skip = true }, - { message = "^chore: (release|bump|update changelog)", skip = true }, - { message = "^deps", group = "Dependencies" }, - { message = "^chore", group = "Miscellaneous" }, - { body = ".*security", group = "Security" }, -] - -protect_breaking_commits = false -filter_commits = false -tag_pattern = "v[0-9].*" -skip_tags = "v0.0.0" -ignore_tags = "" -topo_order = false -sort_commits = "oldest" diff --git a/codecov.yml b/codecov.yml index c82e1975..bd6f96a8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,13 +1,15 @@ +# Coverage remains informational: 95% is the ratchet target, not a merge gate. +# Measured 2026-09-15: 94.73% lines, 94.44% regions, 93.73% functions. coverage: status: project: default: target: 95% - informational: true # report-only: never fails CI (baseline ~72%, iron rule: local green => CI green) + informational: true patch: default: target: 95% - informational: true # new code encouraged to 95%, still non-blocking + informational: true comment: layout: "reach, diff, flags, files" diff --git a/crates/codegraph-cli/Cargo.toml b/crates/codegraph-cli/Cargo.toml index b6a6add6..a9266cf1 100644 --- a/crates/codegraph-cli/Cargo.toml +++ b/crates/codegraph-cli/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-rs" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-core/Cargo.toml b/crates/codegraph-core/Cargo.toml index 3ec6ecc4..96c2af55 100644 --- a/crates/codegraph-core/Cargo.toml +++ b/crates/codegraph-core/Cargo.toml @@ -2,6 +2,7 @@ name = "codegraph-core" description = "Core domain types, node-id/content hashing, config, and logging for CodeGraph-rs." version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-daemon/Cargo.toml b/crates/codegraph-daemon/Cargo.toml index 92132ad8..645878f3 100644 --- a/crates/codegraph-daemon/Cargo.toml +++ b/crates/codegraph-daemon/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-daemon" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-extract/Cargo.toml b/crates/codegraph-extract/Cargo.toml index 463435f6..7d53792e 100644 --- a/crates/codegraph-extract/Cargo.toml +++ b/crates/codegraph-extract/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-extract" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-graph/Cargo.toml b/crates/codegraph-graph/Cargo.toml index 15830028..93140f44 100644 --- a/crates/codegraph-graph/Cargo.toml +++ b/crates/codegraph-graph/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-graph" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-mcp/Cargo.toml b/crates/codegraph-mcp/Cargo.toml index 79ff9b15..b95947d4 100644 --- a/crates/codegraph-mcp/Cargo.toml +++ b/crates/codegraph-mcp/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-mcp" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-resolve/Cargo.toml b/crates/codegraph-resolve/Cargo.toml index e70dade0..ebc66721 100644 --- a/crates/codegraph-resolve/Cargo.toml +++ b/crates/codegraph-resolve/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-resolve" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-store/Cargo.toml b/crates/codegraph-store/Cargo.toml index 207283f4..6875c0df 100644 --- a/crates/codegraph-store/Cargo.toml +++ b/crates/codegraph-store/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-store" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/crates/codegraph-watch/Cargo.toml b/crates/codegraph-watch/Cargo.toml index 08644b01..f52b5205 100644 --- a/crates/codegraph-watch/Cargo.toml +++ b/crates/codegraph-watch/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "codegraph-watch" version.workspace = true +publish.workspace = true edition.workspace = true license.workspace = true authors.workspace = true diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000..9dc22b22 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,26 @@ +# Documentation instructions + +These rules apply to `docs/**` and supplement the root [`AGENTS.md`](../AGENTS.md). + +- Write canonical technical references in English and describe current AS-BUILT + behavior. A design, plan, issue, or upstream behavior is not shipped behavior. +- Update the canonical page alongside the code. README is a landing page, not a + second CLI, MCP, language, installer, or architecture reference. +- Keep [`../README.md`](../README.md) and + [`readme/README.zh-CN.md`](readme/README.zh-CN.md) structurally synchronized: + same commands, links, security posture, distribution paths, and claims. +- Avoid volatile counts, dependency versions, runner labels, protocol matrices, + and coverage percentages in prose. When a count is valuable, derive it from + source and lock it in `scripts/docs-check.py` or a product test. +- Never claim latency, memory, throughput, or scale without a reproducible result + that names the commit, corpus, environment, command, run count, and dispersion. +- Keep relative links repository-portable. Run `python3 scripts/docs-check.py` + after editing headings or links and `oxfmt --check` before handoff. +- Treat `upstream-sync/UPSTREAM.md` log entries and dated audit reports as + append-only evidence. Update the mutable Current alignment block and add a new + dated entry; do not rewrite older conclusions with present-day facts. +- Golden regeneration instructions must name the corpus, exact commands, expected + changed artifacts, and why the graph change is intentional. +- A future UI design is documented as a proposal until the owner selects a Penpot + direction and a code PR lands. Do not turn design-session status into product + documentation. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d5df20e1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,50 @@ +# CodeGraph documentation + +The root [README](../README.md) is the landing page. This index points to the +canonical technical references; update the owning page rather than duplicating +volatile behavior elsewhere. + +## Use CodeGraph + +- [CLI reference](cli.md) — command/path contracts, installation targets, + daemon/watch lifecycle, configuration, and diagnostics +- [MCP reference](mcp.md) — tool schemas, visible/known tools, project resolution, + stdio and streamable HTTP, protocol behavior, and client configuration +- [Supported languages](languages.md) — user-facing language coverage, extensions, + extraction tiers, and static-analysis boundaries +- [Godot static analysis](godot.md) — scenes, resources, scripts, autoloads, + honesty signals, and known runtime boundaries +- [Troubleshooting](troubleshooting.md) — opt-in JSONL diagnostics and evidence to + include in a report + +## Understand the implementation + +- [Architecture](architecture.md) — workspace dependency graph and the extraction, + storage, resolution, query, MCP, daemon, and watcher flow +- [Data model](data-model.md) — SQLite/FTS5 tables, indexes, migrations, and + connection policy +- [Equivalence oracle](equivalence.md) — stable IDs, canonicalization tiers, + golden corpora, and regeneration procedures +- [Grammar ABI manifest](grammar-manifest.md) — grammar/custom extractor ownership + and contributor checks +- [Embedded extraction](embedded-extraction.md) — Vue, Svelte, Astro, Razor, + Liquid, MyBatis XML, and DFM/FMX delegation +- [Benchmark methodology](benchmark.md) and + [current result status](benchmark-results.md) — how performance evidence is + collected and what is currently safe to claim + +## Project governance + +- [Contributing](../CONTRIBUTING.md) +- [Security policy](../SECURITY.md) +- [Code of Conduct](../CODE_OF_CONDUCT.md) +- [Agent contributor contract](../AGENTS.md) +- [Upstream synchronization ledger](upstream-sync/UPSTREAM.md) +- [Dated upstream audits](upstream-sync/) + +## Language policy + +English is canonical for deep technical references. The +[Chinese README](readme/README.zh-CN.md) mirrors the landing page, commands, +security posture, and navigation; it does not duplicate every implementation +reference. diff --git a/docs/architecture.md b/docs/architecture.md index 7b6dd12e..22d25d4f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -228,7 +228,7 @@ FTS5 候选(`search_nodes_fts_filtered`)→(空且文本≥2)LIKE 阶梯 ## 7. CLI 表面 / CLI Surface(`codegraph-cli`) `codegraph` 单一二进制,clap 26 个子命令(见 -[README §CLI 子命令](../README.md#cli-subcommands))。CLI bootstrap: +[CLI 参考](cli.md))。CLI bootstrap: `Config::load_env_or_default` fail-fast(人类可读错误,只配置 logger,不代表任何项目) → `init_logger`(在 `main` 中持有 guard;日志只走 stderr,`serve --mcp` 独占 stdout 以免污染 JSON-RPC 流)。每个针对项目的操作再各自从该项目解析出的索引根加载其 diff --git a/docs/benchmark-results.md b/docs/benchmark-results.md new file mode 100644 index 00000000..dff21619 --- /dev/null +++ b/docs/benchmark-results.md @@ -0,0 +1,15 @@ +# Benchmark result status + +**No current benchmark snapshot is claimed by this revision.** + +The repository contains the benchmark harness and pinned corpus registry, but the +isolated implementation worktree used for this documentation update did not +contain the ignored fetched corpora or a built pinned upstream CLI. Running only +the Rust arm, inventing an upstream command, or reusing a result from an unnamed +older commit would not satisfy the comparison contract. + +Accordingly, the landing pages make no quantitative latency or throughput claim. +Generate this file from a complete run using the command and evidence checklist in +[`benchmark.md`](benchmark.md). Commit a generated result only when it identifies +both implementation commits, all corpus pins, environment, cache policy, run +count, schema controls, raw samples, and dispersion. diff --git a/docs/benchmark.md b/docs/benchmark.md new file mode 100644 index 00000000..f20f4ad7 --- /dev/null +++ b/docs/benchmark.md @@ -0,0 +1,111 @@ +# Benchmark methodology + +CodeGraph ships a reproducible comparison harness in `codegraph-bench`. It is +separate from correctness: a faster result never relaxes schema, golden, or graph +parity. + +## What the pipeline measures + +For each pinned corpus, the harness compares the release Rust CLI with a built +copy of the pinned upstream TypeScript CLI on byte-identical source trees. It +records: + +- cold and warm full-index wall time; +- one-file incremental synchronization wall time; +- cold and warm query latency (median, MAD, p50, and p99); +- peak RSS samples when the host exposes them; +- resulting database size; and +- a Rust-only in-process parse metric, clearly separated because upstream has no + equivalent entry point. + +Before timing a corpus, the pipeline indexes both arms and requires normalized +SQLite `.schema` equality. A schema mismatch aborts the run rather than comparing +non-equivalent work. + +## Pinned corpora + +The source registry in `crates/codegraph-bench/src/corpus.rs` is authoritative. +Each row pins a repository URL, full commit SHA, benchmark subdirectory, expected +non-blank LOC, and source-file count. `--fetch-corpora` creates immutable +checkouts under ignored `bench/corpora/` and refuses an existing checkout at the +wrong commit. + +Inspect the registry and local state: + +```bash +cargo run --locked -p codegraph-bench --bin bench -- --list-corpora +``` + +Fetch the exact commits (networked, explicit operation): + +```bash +cargo run --locked -p codegraph-bench --bin bench -- --fetch-corpora +``` + +## Prerequisites + +1. Build the shipped Rust binary with the same locked release profile used by + release workflows: + + ```bash + cargo build --locked --release -p codegraph-rs + ``` + +2. Materialize the pinned upstream checkout at `reference/colby` and build + `reference/colby/dist/bin/codegraph.js` according to that checkout's + `RUN.md`. Record its exact commit in the result artifact. +3. Fetch the corpus pins and verify `--list-corpora` reports the expected commit, + file count, and LOC for every selected corpus. +4. Use an otherwise idle machine. Record CPU, memory, OS, kernel, Node, Rust, + Rust implementation commit, and upstream implementation commit. + +The full pipeline is Linux-oriented because peak-RSS collection and cold-cache +handling use Linux facilities when available. If the kernel does not allow page +cache drops, the harness records best-effort userspace eviction; do not label +that series equivalent to a privileged cold-cache run without the emitted caveat. + +## Run + +Use at least two runs because run one is discarded. A normal evidence run should +use enough samples for stable p99 interpretation; twelve is the CLI default. + +```bash +cargo run --locked --release -p codegraph-bench --bin bench -- \ + --run \ + --runs 12 \ + --corpora all \ + --out target/benchmark-results.json \ + --report-md docs/benchmark-results.md +``` + +To isolate one corpus during harness development: + +```bash +cargo run --locked --release -p codegraph-bench --bin bench -- \ + --run --runs 12 --corpora fd-small \ + --out target/benchmark-fd.json +``` + +To re-render Markdown without rerunning measurements: + +```bash +cargo run --locked -p codegraph-bench --bin bench -- \ + --render-md target/benchmark-results.json docs/benchmark-results.md +``` + +## Evidence requirements + +A publishable result must include: + +- raw JSON and generated Markdown from the same run; +- clean Git status or an explicit diff description; +- both implementation commit SHAs; +- exact corpus pins and observed counts; +- environment fields emitted by the harness; +- run count, discarded-first policy, and cache mode; +- schema equality for every corpus; +- every raw sample, median, MAD, p50, and p99; +- failures, timeouts, unsupported RSS, or cache-eviction caveats. + +Do not copy a latency number into README, AGENTS, or release notes without linking +to such an artifact. Results are snapshots, not timeless product guarantees. diff --git a/docs/data-model.md b/docs/data-model.md index d035cd57..31019438 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -1,253 +1,211 @@ -# CodeGraph SQLite/FTS5 Data Model Parity Spec +# SQLite and FTS5 data model -> AS-BUILT 校核(T27):本文与已提交代码一致。`CURRENT_SCHEMA_VERSION = 5` 与 -> `FRESH_SCHEMA_DESCRIPTION = "Initial schema includes all migrations"` -> (`crates/codegraph-store/src/migrations.rs`)、7 个连接 PRAGMA -> (`connection.rs`,`configureConnection` 步骤的顺序)、`nodes.return_type` -> 列与迁移序列均按本文实现。 +This is the AS-BUILT storage contract owned by `codegraph-store`. -This document records the authoritative storage contract for the Rust port. The golden schema and database were captured from the reference implementation run against the deterministic mini fixture at `crates/codegraph-bench/fixtures/mini/`, copied to `/tmp/cg-fixture-mini/` for indexing. +- Current schema version: **8** +- Base DDL: `crates/codegraph-store/src/schema.rs` +- Ordered migrations: `crates/codegraph-store/src/migrations.rs` +- Connection and lease policy: `crates/codegraph-store/src/connection.rs` +- Canonical schema oracle: `reference/golden/*/schema.sql` and + `crates/codegraph-store/tests/schema_parity.rs` -Golden artifacts captured from that run, committed under `reference/golden/`: +`scripts/docs-check.py` derives the version from source and checks this document, +so version prose cannot silently drift. -- Schema dump: `reference/golden/colby.schema.sql` -- Raw database: `reference/golden/mini/colby.db` -- Nodes table JSON: `reference/golden/mini/colby.nodes.json` -- Database location: `/tmp/cg-fixture-mini/.codegraph/codegraph.db` +## Initialization and migration -## Real `.schema` Dump Contract +A new database is configured before its first page is written, then receives the +complete base schema. The base DDL inserts schema row `1`; initialization also +records the current version with description `Initial schema includes all +migrations`. Existing databases run every missing migration in ascending order, +each in its own transaction. -`reference/golden/colby.schema.sql` was generated with: +Migrations that add a column first inspect `PRAGMA table_info`. This makes replay +safe when a newer base schema already contains the column while still failing +loudly if the table itself is absent. -```bash -sqlite3 /tmp/cg-fixture-mini/.codegraph/codegraph.db .schema > reference/golden/colby.schema.sql -``` +| Version | Change | +| ------: | ---------------------------------------------------------------------------- | +| 1 | Core nodes, edges, files, unresolved references, FTS5, triggers, and indexes | +| 2 | `project_metadata`; unresolved-ref file/language context; edge provenance | +| 3 | `idx_nodes_lower_name` expression index | +| 4 | Drop redundant single-column edge source/target indexes | +| 5 | `nodes.return_type` for receiver-type inference | +| 6 | `unresolved_refs.reference_subkind` for structural/framework evidence | +| 7 | Deduplicate edges and add their unique semantic identity index | +| 8 | `files.generated` plus the partial generated-file index | -The dump is post-initialization/post-migration truth. It includes the six application tables (`schema_versions`, `nodes`, `edges`, `files`, `unresolved_refs`, `project_metadata`), `nodes_fts` plus its FTS5 shadow tables, the three FTS triggers, the post-migration indexes, `sqlite_sequence`, and `sqlite_stat1` created by the maintenance/ANALYZE path. +Schema version and extraction version are independent. A DDL change needs a +schema migration; a change in graph meaning may need an extraction-version move +even when DDL is unchanged. -Notably absent in the post-migration state: `idx_edges_source` and `idx_edges_target`. Fresh schema omits them and migration v4 drops them from older databases. - -## Tables and Columns +## Application tables ### `schema_versions` -| Column | Type | Null | Default | Key | Notes | -| ------------- | --------- | ---------------------: | ------- | ------------- | ------------------------------------- | -| `version` | `INTEGER` | nullable by SQLite DDL | none | `PRIMARY KEY` | Schema version number. | -| `applied_at` | `INTEGER` | `NOT NULL` | none | | Milliseconds since epoch. | -| `description` | `TEXT` | nullable | none | | Migration/initialization description. | - -Fresh DB rows from the run: version `1` (`Initial schema`) and version `5` (`Initial schema includes all migrations`). +| Column | Type | Contract | +| ------------- | ------------------- | ---------------------- | +| `version` | INTEGER primary key | applied schema version | +| `applied_at` | INTEGER not null | epoch milliseconds | +| `description` | TEXT nullable | migration description | ### `nodes` -| Column | Type | Null | Default | Key | Notes | -| ----------------- | --------- | ---------------------: | ------- | ------------- | ----------------------------------------------------------------------------- | -| `id` | `TEXT` | nullable by SQLite DDL | none | `PRIMARY KEY` | Stable node identifier. | -| `kind` | `TEXT` | `NOT NULL` | none | | Symbol/file/import kind. | -| `name` | `TEXT` | `NOT NULL` | none | | Display/local name. | -| `qualified_name` | `TEXT` | `NOT NULL` | none | | Qualified symbol name. | -| `file_path` | `TEXT` | `NOT NULL` | none | | Project-relative path. | -| `language` | `TEXT` | `NOT NULL` | none | | Language label. | -| `start_line` | `INTEGER` | `NOT NULL` | none | | 1-based start line. | -| `end_line` | `INTEGER` | `NOT NULL` | none | | 1-based end line. | -| `start_column` | `INTEGER` | `NOT NULL` | none | | Start column as emitted by the extractor. | -| `end_column` | `INTEGER` | `NOT NULL` | none | | End column as emitted by the extractor. | -| `docstring` | `TEXT` | nullable | none | | Optional documentation string. | -| `signature` | `TEXT` | nullable | none | | Optional signature. | -| `visibility` | `TEXT` | nullable | none | | Optional visibility (`private`, etc.). | -| `is_exported` | `INTEGER` | nullable | `0` | | Boolean encoded as integer. | -| `is_async` | `INTEGER` | nullable | `0` | | Boolean encoded as integer. | -| `is_static` | `INTEGER` | nullable | `0` | | Boolean encoded as integer. | -| `is_abstract` | `INTEGER` | nullable | `0` | | Boolean encoded as integer. | -| `decorators` | `TEXT` | nullable | none | | JSON array. | -| `type_parameters` | `TEXT` | nullable | none | | JSON array. | -| `return_type` | `TEXT` | nullable | none | | Normalized return/result type name; present in fresh schema and migration v5. | -| `updated_at` | `INTEGER` | `NOT NULL` | none | | Milliseconds since epoch. | +| Column | Type | Contract | +| ----------------------------------------------------- | ----------------- | -------------------------------------------- | +| `id` | TEXT primary key | stable node ID | +| `kind` | TEXT not null | serialized `NodeKind` | +| `name` | TEXT not null | display/local name | +| `qualified_name` | TEXT not null | language/container-qualified name | +| `file_path` | TEXT not null | project-relative `/` path | +| `language` | TEXT not null | serialized `Language` | +| `start_line`, `end_line` | INTEGER not null | 1-based source lines | +| `start_column`, `end_column` | INTEGER not null | extractor columns | +| `docstring` | TEXT nullable | documentation text | +| `signature` | TEXT nullable | callable/declaration signature | +| `visibility` | TEXT nullable | language visibility | +| `is_exported`, `is_async`, `is_static`, `is_abstract` | INTEGER default 0 | boolean flags | +| `decorators`, `type_parameters` | TEXT nullable | JSON arrays | +| `return_type` | TEXT nullable | normalized result type (migration 5) | +| `updated_at` | INTEGER not null | epoch milliseconds; canonicalizer removes it | + +The ordinary symbol ID formula is documented in +[`equivalence.md`](equivalence.md). Framework-created nodes with an explicitly +documented compatibility ID are the exception, not a second general formula. ### `edges` -| Column | Type | Null | Default | Key | Notes | -| ------------ | --------- | ---------------------: | ------- | --------------------------- | -------------------------------------------------------------------- | -| `id` | `INTEGER` | nullable by SQLite DDL | none | `PRIMARY KEY AUTOINCREMENT` | Row id. Creates `sqlite_sequence`. | -| `source` | `TEXT` | `NOT NULL` | none | FK | References `nodes(id)` with `ON DELETE CASCADE`. | -| `target` | `TEXT` | `NOT NULL` | none | FK | References `nodes(id)` with `ON DELETE CASCADE`. | -| `kind` | `TEXT` | `NOT NULL` | none | | Relationship kind. | -| `metadata` | `TEXT` | nullable | none | | JSON object. | -| `line` | `INTEGER` | nullable | none | | Source line for relationship if known. | -| `col` | `INTEGER` | nullable | none | | Source column for relationship if known. | -| `provenance` | `TEXT` | nullable | `NULL` | | Added by migration v2; provenance tag for heuristic/synthetic edges. | +| Column | Type | Contract | +| ------------------ | --------------------------------- | ---------------------------------------------- | +| `id` | INTEGER primary key autoincrement | row identity; not semantic | +| `source`, `target` | TEXT not null | foreign keys to `nodes(id)`, cascade on delete | +| `kind` | TEXT not null | serialized `EdgeKind` | +| `metadata` | TEXT nullable | JSON object | +| `line`, `col` | INTEGER nullable | source location when known | +| `provenance` | TEXT nullable default NULL | resolution/synthesis provenance | + +An edge's semantic identity is: + +```text +(source, target, kind, IFNULL(line, -1), IFNULL(col, -1)) +``` + +`idx_edges_identity` is unique on that tuple. `INSERT OR IGNORE` therefore +removes repeat-pass duplicates, including coordinate-less edges. ### `files` -| Column | Type | Null | Default | Key | Notes | -| -------------- | --------- | ---------------------: | ------- | ------------- | ------------------------------------ | -| `path` | `TEXT` | nullable by SQLite DDL | none | `PRIMARY KEY` | Project-relative source path. | -| `content_hash` | `TEXT` | `NOT NULL` | none | | File content hash. | -| `language` | `TEXT` | `NOT NULL` | none | | Language label. | -| `size` | `INTEGER` | `NOT NULL` | none | | File size in bytes. | -| `modified_at` | `INTEGER` | `NOT NULL` | none | | File modified timestamp. | -| `indexed_at` | `INTEGER` | `NOT NULL` | none | | Index timestamp. | -| `node_count` | `INTEGER` | nullable | `0` | | Number of indexed nodes in the file. | -| `errors` | `TEXT` | nullable | none | | JSON array. | +| Column | Type | Contract | +| --------------------------- | -------------------------- | ----------------------------------------------- | +| `path` | TEXT primary key | project-relative path | +| `content_hash` | TEXT not null | full SHA-256 content digest | +| `language` | TEXT not null | serialized language ID | +| `size` | INTEGER not null | bytes | +| `modified_at`, `indexed_at` | INTEGER not null | epoch milliseconds | +| `node_count` | INTEGER default 0 | extracted nodes for the file | +| `errors` | TEXT nullable | JSON array of extraction errors | +| `generated` | INTEGER not null default 0 | path/header generated-file signal (migration 8) | + +Migration 8 deliberately does not read source to backfill `generated`; older rows +remain zero until re-extraction. Readers combine the stored flag with compatible +path classification where required. ### `unresolved_refs` -| Column | Type | Null | Default | Key | Notes | -| ---------------- | --------- | ---------------------: | ----------- | --------------------------- | ------------------------------------------------ | -| `id` | `INTEGER` | nullable by SQLite DDL | none | `PRIMARY KEY AUTOINCREMENT` | Row id. | -| `from_node_id` | `TEXT` | `NOT NULL` | none | FK | References `nodes(id)` with `ON DELETE CASCADE`. | -| `reference_name` | `TEXT` | `NOT NULL` | none | | Unresolved symbol/reference name. | -| `reference_kind` | `TEXT` | `NOT NULL` | none | | Reference type. | -| `line` | `INTEGER` | `NOT NULL` | none | | Reference line. | -| `col` | `INTEGER` | `NOT NULL` | none | | Reference column. | -| `candidates` | `TEXT` | nullable | none | | JSON array. | -| `file_path` | `TEXT` | `NOT NULL` | `''` | | Added by migration v2; present in fresh schema. | -| `language` | `TEXT` | `NOT NULL` | `'unknown'` | | Added by migration v2; present in fresh schema. | +| Column | Type | Contract | +| ---------------------------------- | --------------------------------- | --------------------------------------------- | +| `id` | INTEGER primary key autoincrement | row identity | +| `from_node_id` | TEXT not null | foreign key to source node, cascade on delete | +| `reference_name`, `reference_kind` | TEXT not null | unresolved target evidence | +| `line`, `col` | INTEGER not null | reference location | +| `candidates` | TEXT nullable | JSON array | +| `file_path` | TEXT not null default `''` | source file context (migration 2) | +| `language` | TEXT not null default `unknown` | source language (migration 2) | +| `reference_subkind` | TEXT nullable | structural/framework label (migration 6) | ### `project_metadata` -| Column | Type | Null | Default | Key | Notes | -| ------------ | --------- | ---------------------: | ------- | ------------- | ------------------------- | -| `key` | `TEXT` | nullable by SQLite DDL | none | `PRIMARY KEY` | Metadata key. | -| `value` | `TEXT` | `NOT NULL` | none | | Metadata value. | -| `updated_at` | `INTEGER` | `NOT NULL` | none | | Milliseconds since epoch. | - -### SQLite/FTS Internal Tables in `.schema` - -- `sqlite_sequence(name,seq)` appears because `edges` and `unresolved_refs` use `AUTOINCREMENT`. -- `sqlite_stat1(tbl,idx,stat)` appears because the maintenance path runs `PRAGMA optimize` after bulk writes. -- FTS5 shadow tables generated by `nodes_fts`: `nodes_fts_data`, `nodes_fts_idx`, `nodes_fts_docsize`, `nodes_fts_config`. +| Column | Type | Contract | +| ------------ | ---------------- | ------------------ | +| `key` | TEXT primary key | metadata name | +| `value` | TEXT not null | serialized value | +| `updated_at` | INTEGER not null | epoch milliseconds | -## Indexes (Post-Migration) +## FTS5 -Application indexes in the authoritative dump: +`nodes_fts` is an external-content FTS5 table over `nodes`: ```sql -CREATE INDEX idx_nodes_kind ON nodes(kind); -CREATE INDEX idx_nodes_name ON nodes(name); -CREATE INDEX idx_nodes_qualified_name ON nodes(qualified_name); -CREATE INDEX idx_nodes_file_path ON nodes(file_path); -CREATE INDEX idx_nodes_language ON nodes(language); -CREATE INDEX idx_nodes_file_line ON nodes(file_path, start_line); -CREATE INDEX idx_nodes_lower_name ON nodes(lower(name)); -CREATE INDEX idx_edges_kind ON edges(kind); -CREATE INDEX idx_edges_source_kind ON edges(source, kind); -CREATE INDEX idx_edges_target_kind ON edges(target, kind); -CREATE INDEX idx_files_language ON files(language); -CREATE INDEX idx_files_modified_at ON files(modified_at); -CREATE INDEX idx_unresolved_from_node ON unresolved_refs(from_node_id); -CREATE INDEX idx_unresolved_name ON unresolved_refs(reference_name); -CREATE INDEX idx_unresolved_file_path ON unresolved_refs(file_path); -CREATE INDEX idx_unresolved_from_name ON unresolved_refs(from_node_id, reference_name); -CREATE INDEX idx_edges_provenance ON edges(provenance); +CREATE VIRTUAL TABLE nodes_fts USING fts5( + id, + name, + qualified_name, + docstring, + signature, + content='nodes', + content_rowid='rowid' +); ``` -SQLite also creates autoindexes for primary keys: `sqlite_autoindex_nodes_1`, `sqlite_autoindex_files_1`, and `sqlite_autoindex_project_metadata_1`. +`nodes_ai`, `nodes_ad`, and `nodes_au` maintain FTS rows after node insert, +delete, and update. No tokenizer is specified, so the bundled SQLite FTS5 default +applies. The bundled build must support FTS5; schema creation fails instead of +silently switching to a weaker search backend. -## FTS5 Configuration +FTS5 shadow tables and SQLite-owned `sqlite_sequence`/`sqlite_stat1` can appear in +`.schema`. The canonicalizer accounts for the expected internal objects while +preserving application DDL. -Authoritative virtual table DDL: +## Application indexes -```sql -CREATE VIRTUAL TABLE nodes_fts USING fts5( - id, - name, - qualified_name, - docstring, - signature, - content='nodes', - content_rowid='rowid' -) -/* nodes_fts(id,name,qualified_name,docstring,signature) */; -``` +The base schema creates: -- FTS engine: SQLite FTS5. -- Indexed columns: `id`, `name`, `qualified_name`, `docstring`, `signature`. -- External content table: `content='nodes'`. -- External content row id: `content_rowid='rowid'`. -- Tokenizer: no explicit tokenizer in the DDL, so SQLite's FTS5 default tokenizer (`unicode61`) applies. +- node indexes on kind, name, qualified name, file path, language, + `(file_path, start_line)`, and `lower(name)`; +- edge indexes on kind, `(source, kind)`, `(target, kind)`, provenance, and the + unique semantic identity tuple; +- file indexes on language, modified time, and the partial `generated = 1` set; +- unresolved-reference indexes on source node, name, file path, and + `(from_node_id, reference_name)`. -## FTS Trigger Bodies (Verbatim) +The former `idx_edges_source` and `idx_edges_target` are intentionally absent: +the composite indexes cover their left-prefix lookups and migration 4 removes +them from old databases. -```sql -CREATE TRIGGER nodes_ai AFTER INSERT ON nodes BEGIN - INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) - VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); -END; -``` +## Connection policy -```sql -CREATE TRIGGER nodes_ad AFTER DELETE ON nodes BEGIN - INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) - VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); -END; -``` +Normal write-capable connections are configured in this order: ```sql -CREATE TRIGGER nodes_au AFTER UPDATE ON nodes BEGIN - INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature) - VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature); - INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature) - VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature); -END; +busy_timeout = 5000 ms +foreign_keys = ON +journal_mode = WAL +journal_size_limit = CODEGRAPH_WAL_VALVE_MB (default 256 MiB) +synchronous = NORMAL +cache_size = -64000 +temp_store = MEMORY +mmap_size = 268435456 ``` -## Migration Sequence - -Migration history: - -| Version | Description | Effect | -| ------: | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | Initial schema | Loaded from `schema.sql`; creates core tables, FTS5 table, triggers, indexes, and inserts version 1. | -| 2 | Add project metadata, provenance tracking, and unresolved ref context | Creates `project_metadata`; adds `unresolved_refs.file_path TEXT NOT NULL DEFAULT ''`, `unresolved_refs.language TEXT NOT NULL DEFAULT 'unknown'`, `edges.provenance TEXT DEFAULT NULL`; creates `idx_unresolved_file_path` and `idx_edges_provenance`. | -| 3 | Add lower(name) expression index for memory-efficient case-insensitive lookups | Creates `idx_nodes_lower_name ON nodes(lower(name))`. | -| 4 | Drop redundant idx_edges_source / idx_edges_target (covered by source_kind / target_kind composites) | Drops `idx_edges_source` and `idx_edges_target` if present. They must not exist in the Rust post-migration schema. | -| 5 | Add nodes.return_type — normalized return/result type for receiver-type inference (C++ singletons/factories, #645) | Adds `nodes.return_type TEXT` for older DBs. Fresh schema already contains it. | +A rebuild validates its exclusive `IndexLease` between file-touching PRAGMAs. +Bulk indexing temporarily uses `synchronous=OFF`, a larger cache/mmap window, and +normally disables WAL autocheckpoint; a fallible finalizer restores defaults, +compacts, and publishes the completed namespace. Early failure leaves the +building namespace unreadable rather than publishing a partial database. -`CURRENT_SCHEMA_VERSION` is `5`. On fresh initialization, the store loads schema.sql and then records version `5` as `Initial schema includes all migrations` so old migrations are not replayed. +Readers and writers are state-gated. The permanent lock file is a capability tied +to the resolved database parent; a path string or stale PID alone is not write +authority. -## Connection PRAGMAs +## Canonical schema proof -Every connection is configured (the `configureConnection` step) with: +Run the store tests and full oracle rather than comparing prose: -```sql -PRAGMA busy_timeout = 5000; -PRAGMA foreign_keys = ON; -PRAGMA journal_mode = WAL; -PRAGMA synchronous = NORMAL; -PRAGMA cache_size = -64000; -PRAGMA temp_store = MEMORY; -PRAGMA mmap_size = 268435456; -``` - -Maintenance after bulk writes is best-effort: - -```sql -PRAGMA optimize; -PRAGMA wal_checkpoint(PASSIVE); +```bash +cargo test --locked -p codegraph-store schema_parity +cargo test --locked -p codegraph-bench --test equivalence ``` -`sqlite-adapter.ts` implements `.pragma()` by executing `PRAGMA = ` for write pragmas and reading `PRAGMA ` for read pragmas using Node's built-in `node:sqlite` `DatabaseSync`. - -Live external `sqlite3` query results against the golden reference `.db` under `reference/golden/mini/` after the run: - -| Query | Result | Persistence note | -| -------------------------- | -------- | --------------------------------------------------------------------------------------- | -| `select sqlite_version();` | `3.52.0` | Version of the external CLI used for capture, not the application's embedded SQLite. | -| `PRAGMA journal_mode;` | `wal` | Persisted in the database. | -| `PRAGMA synchronous;` | `2` | External sqlite3 connection default (`FULL`); the application connection sets `NORMAL`. | -| `PRAGMA foreign_keys;` | `0` | Connection-local; the application connection sets `ON`. | -| `PRAGMA busy_timeout;` | `0` | Connection-local; the application connection sets `5000`. | -| `PRAGMA cache_size;` | `-2000` | Connection-local external default; the application connection sets `-64000`. | -| `PRAGMA temp_store;` | `0` | Connection-local external default; the application connection sets `MEMORY`. | -| `PRAGMA mmap_size;` | `0` | Connection-local external default; the application connection sets `268435456`. | - -## Rust `rusqlite` Parity Plan - -- Workspace dependency is `rusqlite = { version = "0.31", features = ["bundled"] }`. -- Resolved `libsqlite3-sys` is `0.28.0`. -- The vendored bundled SQLite in `libsqlite3-sys 0.28.0` is SQLite `3.45.0` (`SQLITE_VERSION_NUMBER 3045000`, source id `2024-01-15 17:01:13 1066602b2b1976fe58b5150777cced894af17c803e068f5918390d6915b46e1d`). -- The Rust store must enable the same connection PRAGMAs on every opened connection before schema work or queries. -- The bundled SQLite build must support FTS5; task decisions already require `rusqlite` bundled for FTS5. If future CI disables FTS5, schema creation must fail loudly rather than silently degrading. -- Rust schema application must produce the same post-migration `.schema` as `reference/golden/colby.schema.sql`, including FTS5 external-content config and the three trigger bodies. +The oracle normalizes schema syntax consistently, but it does not make SQLite +file bytes reproducible. Database page counters, WAL state, and row IDs are not +canonical graph output; committed `schema.sql` and canonical JSON are. diff --git a/docs/grammar-manifest.md b/docs/grammar-manifest.md index 290f578e..9a4e70a6 100644 --- a/docs/grammar-manifest.md +++ b/docs/grammar-manifest.md @@ -1,125 +1,128 @@ -# Tree-sitter grammar ABI manifest - -Task 5 smoke target: `tree-sitter = "0.26"` from the workspace. The authoritative language set is the `LANGUAGES` table plus the grammar manifest (WASM_GRAMMAR_FILES, extension delegation, custom/file-level branches). `unknown` is intentionally excluded from dependency planning. - -The current `LANGUAGES` set has 30 entries including `unknown`; this manifest accounts for the 29 concrete entries, `dfm` as the Pascal custom extension path, and the user-requested future/static-resource grammars `sql`, `html`, `css`, and `json`. In this language set, `sql`, `html`, `css`, and general `json` are not `Language` enum entries; Shopify `templates/*.json` and `sections/*.json` delegate to Liquid. - -## Tier policy - -| Tier | Meaning | -| ------ | --------------------------------------------------------------------------------------------------------------- | -| a | crates.io grammar crate links directly with workspace `tree-sitter = "0.26"` and passes `abi_smoke` | -| b | git dependency pinned by rev and passes `abi_smoke` | -| c | vendored grammar plan required; task 17 should add generated C/CPP sources via `cc` | -| custom | no native grammar dependency selected because the language uses custom/file-level extraction or host delegation | - -## Manifest - -| Language | Upstream source status | Chosen strategy | Tier | ABI status | Notes | -| ---------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | -| typescript | WASM `tree-sitter-typescript.wasm` | `tree-sitter-typescript = "0.23.2"`, `LANGUAGE_TYPESCRIPT` | a | PASS | Crate exposes both TypeScript and TSX grammars. | -| javascript | WASM `tree-sitter-javascript.wasm` | `tree-sitter-javascript = "0.25.0"`, `LANGUAGE` | a | PASS | Used for `.js`, `.mjs`, `.cjs`, `.xsjs`, `.xsjslib`. | -| tsx | WASM `tree-sitter-tsx.wasm` | `tree-sitter-typescript = "0.23.2"`, `LANGUAGE_TSX` | a | PASS | Same crate as TypeScript. | -| jsx | WASM reuses `tree-sitter-javascript.wasm` | `tree-sitter-javascript = "0.25.0"`, `LANGUAGE` | a | PASS | JSX is parsed by the JavaScript grammar. | -| python | WASM `tree-sitter-python.wasm` | `tree-sitter-python = "0.25.0"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| go | WASM `tree-sitter-go.wasm` | `tree-sitter-go = "0.25.0"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| rust | WASM `tree-sitter-rust.wasm` | `tree-sitter-rust = "0.24.2"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| java | WASM `tree-sitter-java.wasm` | `tree-sitter-java = "0.23.5"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| c | WASM `tree-sitter-c.wasm` | `tree-sitter-c = "0.24.2"`, `LANGUAGE` | a | PASS | `.h` may be promoted to C++/ObjC by source heuristics. | -| cpp | WASM `tree-sitter-cpp.wasm` | `tree-sitter-cpp = "0.23.4"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| csharp | WASM `tree-sitter-c_sharp.wasm` (WASM grammar) | `tree-sitter-c-sharp = "0.23.5"`, `LANGUAGE` | a | PASS | Crates.io release parses primary-constructor smoke under TS 0.26 ABI. | -| razor | custom Razor extractor | CUSTOM | custom | CUSTOM | `.cshtml` / `.razor`; markup is not parsed via tree-sitter. C# snippets can reuse `tree-sitter-c-sharp` later. | -| php | WASM `tree-sitter-php.wasm` | `tree-sitter-php = "0.24.2"`, `LANGUAGE_PHP` | a | PASS | Use PHP grammar, not `LANGUAGE_PHP_ONLY`, for regular files. | -| ruby | WASM `tree-sitter-ruby.wasm` | `tree-sitter-ruby = "0.23.1"`, `LANGUAGE` | a | PASS | Direct crates.io grammar. | -| swift | WASM `tree-sitter-swift.wasm` | `tree-sitter-swift = "0.7.3"`, `LANGUAGE` | a | PASS | Verified against 0.26 despite historical old-core risk; crate uses `tree-sitter-language` and only dev-depends on older core. | -| kotlin | WASM `tree-sitter-kotlin.wasm` | `tree-sitter-kotlin-ng = "1.1.0"`, `LANGUAGE` | a | PASS | Use `kotlin-ng`; avoid legacy `tree-sitter-kotlin = 0.3.8` for this workspace. | -| dart | WASM `tree-sitter-dart.wasm` | `tree-sitter-dart = "0.2.0"`, `LANGUAGE` | a | PASS | Crate is old but exposes `LanguageFn`, so it links with workspace TS 0.26. | -| gdscript | non-upstream Rust-side addition | `tree-sitter-gdscript = "6.1.0"`, `LANGUAGE` | a | PASS | `.gd`; Godot scripting; functions/classes/enums/vars/signals(as Property)/extends/preload-load | -| svelte | custom extractor | CUSTOM | custom | CUSTOM | Delegates script blocks to TypeScript/JavaScript grammars. | -| vue | custom extractor | CUSTOM | custom | CUSTOM | Delegates `