Skip to content

Commit 7f026f5

Browse files
committed
feat: impact resolves callers, reads staged snapshots, bounds git
Agents grepped for "who calls X" because every diffctx entry point needed a change to anchor on. `--symbol NAME|PATH:NAME` (CLI, MCP `symbol`) anchors impact on a name's definitions instead: where it is defined, who calls it, the tests that reach each caller. The MCP tool defaults to impact, which its own description promised; a base install's `diffctx-mcp` names the missing extra instead of dying on `import anyio`, and answers `--version`. A caller was a spelling match: a name in a string or a comment, `re.search` for a changed `search`, `update.message` for `Bot.message`, the other app's `App.tsx`, `legacy/pricing.py` for `shop/pricing.py`. In Python and JS/TS a caller is now resolved through the import that carries it — relative and absolute imports, aliases, re-exports, `require`, `tsconfig` paths read as data — and a method through its receiver: `self`/`this` in the class or a subclass, the class itself, a variable constructed or annotated as it. An unresolved import or receiver is a possible caller with its reason, never a definite one; other languages keep the name model and say so. A name defined everywhere, or a trait method reached through `dyn`, says its callers were not resolved instead of reading as uncalled, and an empty answer is scoped: no resolved static caller in the analysed scope. Test links say what static analysis shows — "reachable from tests", "no static test link found" — never coverage. The alias edges reach the graph too, so pack and locate see an aliased call. `--diff staged` (also `--diff=--cached`, MCP `diff_ref="staged"`) captures the index once as a tree and answers for it alone: an explicit range is now read from its own head tree — the file universe, discovery, the edge builders, the fragments, the stale-reference search — and never from the disk. An unborn branch diffs against the empty tree of its object format, an unmerged index is an error, and impact names the index tree. Impact asks git about the resolved range, so a `24h` window no longer asks about a revision named `24h`. A stalled git could hang a run for hours: every git child now gets what is left of one run deadline, its pipes are read on threads, its whole process group is killed, a cat-file stream that lost its framing is discarded and the file re-read, an interrupted read is reported instead of filled from disk, MCP cancellation stops the native run, and the corpus harness names a case that outlives its limit. The plugin hook keys an answer by a content-addressed result identity, so every spelling of one snapshot shares it and any new byte is answered at once; it records an answer only when delivered, retries a timeout or a partial answer at most twice, reminds at a commit only when its target is exactly what was shown, ignores piped and summary-only views, labels a `-a` or `add && commit` preview as one and checks the commit that lands, logs git hidden in a subshell, fits its answer to 600 tokens, answers a search for an edited name, and reviews --amend against HEAD~1. Python call edges cleared the naming floor almost never: an import confirmed only the fragment that held it. Confirmation is per file, discovery follows the changed files' own imports and no longer pulls in a whole package. Coverage says partial only when a limit touched what the answer is about, and names the file; locate's JSON fits its budget; co-change history is read before the range's head; only a file with a grammar can be unparsed. Also: reverse discovery reads func/fun and Java/C# methods, not locals; the tags fallback no longer links code by syntax words; rescue lets a signature stand in for a caller's body; deleted files and lines still naming a deleted or renamed path are reported; publish-extras restores a pruned tag and a drafted release; the site serves a favicon; Renovate leaves plugin/constraints.txt to uv.lock; setup-uv 10.2.0, tiktoken-rs 0.12.1, thiserror 2.0.21, ruff 0.16.10, pylint 4.0.10; the packaging manifests for 1.18.2. Refs: #300, #306, #313, #317, #319, #321, #330, #332, #333, #334, #335, #336, #337, #338, #339, #340, #341, #342, #343, #344, #345, #346, #347, #348, #349, #350, #351, #353, #354, #356, #360; #355, #357, #358, #359, #361, #362 filed. Claude-Session: https://claude.ai/code/session_01LC9cvWcY1L9YJ1SJB6pBez
1 parent 777f4b4 commit 7f026f5

97 files changed

Lines changed: 8801 additions & 931 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/cd.yml‎

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ jobs:
245245
grep -n -m3 -E '^## \[' CHANGELOG.md
246246
247247
- name: Set up uv
248-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
248+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
249249

250250
# The plugin's uvx launcher pins diffctx; this pins what diffctx pulls
251251
# in, to the set CI installed from uv.lock, so an installed plugin runs
@@ -740,7 +740,7 @@ jobs:
740740
python-version: ${{ matrix.python-version }}
741741

742742
- name: Set up uv
743-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
743+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
744744

745745
- name: Wait for PyPI propagation and install
746746
shell: bash
@@ -813,6 +813,33 @@ jobs:
813813
grep -q "def sub" "${RUNNER_TEMP}/uvx.md"
814814
echo "OK: uvx runs the published wheel"
815815
816+
# A console script cannot depend on an extra, so the base install ships
817+
# diffctx-mcp too; it must answer --version and name the extra rather
818+
# than die on `import anyio` (#333). A fresh venv, so nothing the runner
819+
# or an earlier step installed can stand in for the missing extra.
820+
- name: Smoke - diffctx-mcp from a base install
821+
shell: bash
822+
env:
823+
VERSION: ${{ needs.prepare-version.outputs.version }}
824+
run: |
825+
python -m venv "${RUNNER_TEMP}/base"
826+
bin="${RUNNER_TEMP}/base/bin"
827+
[[ -d "$bin" ]] || bin="${RUNNER_TEMP}/base/Scripts"
828+
"$bin/python" -m pip install --only-binary :all: "diffctx==${VERSION}"
829+
test "$("$bin/diffctx-mcp" --version)" = "diffctx-mcp ${VERSION}"
830+
set +e
831+
"$bin/diffctx-mcp" --help >/dev/null 2>&1
832+
help_rc=$?
833+
"$bin/diffctx-mcp" </dev/null 2>"${RUNNER_TEMP}/base-mcp.err"
834+
rc=$?
835+
set -e
836+
cat "${RUNNER_TEMP}/base-mcp.err"
837+
test "$help_rc" -eq 0
838+
test "$rc" -eq 3
839+
grep -q "diffctx\[mcp\]" "${RUNNER_TEMP}/base-mcp.err"
840+
if grep -q Traceback "${RUNNER_TEMP}/base-mcp.err"; then exit 1; fi
841+
echo "OK: base install's diffctx-mcp names the extra"
842+
816843
# server.json, .mcp.json and the Claude plugin all resolve to
817844
# `uvx --from diffctx[mcp] diffctx-mcp`, but nothing here installed the
818845
# [mcp] extra or ran the console script — so renaming the entry point, or
@@ -935,7 +962,7 @@ jobs:
935962
runs-on: ${{ matrix.os }}
936963
steps:
937964
- name: Set up uv
938-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
965+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
939966

940967
- name: Install from the cp314t wheel, never the sdist
941968
shell: bash
@@ -1118,12 +1145,12 @@ jobs:
11181145
env:
11191146
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11201147
VERSION: ${{ needs.prepare-version.outputs.version }}
1121-
TAG_NAME: ${{ needs.prepare-version.outputs.tag_name }}
1148+
COMMIT_SHA: ${{ needs.prepare-version.outputs.commit_sha }}
11221149
REPO: ${{ github.repository }}
11231150
run: |
11241151
gh workflow run publish-crate.yml \
11251152
--repo "$REPO" --ref main \
1126-
-f version="$VERSION" -f ref="$TAG_NAME"
1153+
-f version="$VERSION" -f ref="$COMMIT_SHA"
11271154
11281155
- name: Wait for the crate publish workflow
11291156
env:
@@ -1145,13 +1172,14 @@ jobs:
11451172
id-token: write
11461173
contents: read
11471174
steps:
1148-
# The release tag, not main: finalize-release commits the bumped
1175+
# The release commit, not main: finalize-release commits the bumped
11491176
# server.json there, and registry versions are immutable — publishing
1150-
# main mid-release would race the bump commit.
1151-
- name: Checkout release tag
1177+
# main mid-release would race the bump commit. Not the tag either: it
1178+
# has been pruned minutes after the push (#332).
1179+
- name: Checkout release commit
11521180
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
11531181
with:
1154-
ref: ${{ needs.prepare-version.outputs.tag_name }}
1182+
ref: ${{ needs.prepare-version.outputs.commit_sha }}
11551183

11561184
# Release assets are named linux_amd64/arm64 (Go convention), NOT the
11571185
# uname -m x86_64/aarch64 — a naive $(uname -m) URL 404s. Pinned by
@@ -1196,10 +1224,10 @@ jobs:
11961224
env:
11971225
IMAGE: ghcr.io/${{ github.repository_owner }}/diffctx
11981226
steps:
1199-
- name: Checkout release tag
1227+
- name: Checkout release commit
12001228
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
12011229
with:
1202-
ref: ${{ needs.prepare-version.outputs.tag_name }}
1230+
ref: ${{ needs.prepare-version.outputs.commit_sha }}
12031231

12041232
- name: Set up Buildx
12051233
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
@@ -1435,7 +1463,7 @@ jobs:
14351463
publish-extras:
14361464
name: Publish npm wrapper and mirror to Docker Hub
14371465
# No edge to update-packaging-manifests: publish-extras.yml re-renders the
1438-
# npm checksums from the release tag, so a Scoop bucket failure can never
1466+
# npm checksums from the release assets, so a Scoop bucket failure can never
14391467
# hold back npm or the Docker Hub mirror.
14401468
needs: [prepare-version, publish-image]
14411469
if: github.event.inputs.publish_to_pypi == 'true'
@@ -1451,11 +1479,13 @@ jobs:
14511479
env:
14521480
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14531481
VERSION: ${{ needs.prepare-version.outputs.version }}
1482+
COMMIT_SHA: ${{ needs.prepare-version.outputs.commit_sha }}
14541483
REPO: ${{ github.repository }}
14551484
run: |
14561485
gh workflow run publish-extras.yml \
14571486
--repo "$REPO" --ref main \
14581487
-f version="$VERSION" \
1488+
-f commit="$COMMIT_SHA" \
14591489
-f publish_npm=true \
14601490
-f mirror_dockerhub=true
14611491

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2626

2727
- name: Set up uv
28-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
28+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
2929
with:
3030
python-version: '3.12'
3131
enable-cache: true
@@ -212,7 +212,7 @@ jobs:
212212
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
213213

214214
- name: Set up uv
215-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
215+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
216216
with:
217217
python-version: '3.10'
218218
enable-cache: true
@@ -372,7 +372,7 @@ jobs:
372372
fetch-depth: 0
373373

374374
- name: Set up uv with Python ${{ matrix.python-version }}
375-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
375+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
376376
with:
377377
python-version: ${{ matrix.python-version }}
378378
enable-cache: true
@@ -462,7 +462,7 @@ jobs:
462462
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
463463

464464
- name: Set up uv
465-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
465+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
466466
with:
467467
python-version: '3.12'
468468
enable-cache: true
@@ -495,7 +495,7 @@ jobs:
495495
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
496496

497497
- name: Set up uv
498-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
498+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
499499
with:
500500
python-version: '3.12'
501501
enable-cache: true
@@ -535,7 +535,7 @@ jobs:
535535
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
536536

537537
- name: Set up uv
538-
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
538+
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
539539
with:
540540
python-version: '3.12'
541541
enable-cache: true

‎.github/workflows/publish-extras.yml‎

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ concurrency:
1313
version:
1414
description: 'Released version to publish downstream (e.g. 1.12.1)'
1515
required: true
16+
commit:
17+
description: 'Release commit SHA (default: the tag, else the "Release version X" commit on main)'
18+
required: false
19+
default: ''
1620
publish_npm:
1721
description: 'Publish the npm wrapper'
1822
required: true
@@ -31,29 +35,109 @@ concurrency:
3135
- 'false'
3236

3337
jobs:
38+
# The release tag has been deleted under a published release (a Forgejo push
39+
# mirror pruned v1.16.0 and v1.18.2 minutes after CD pushed it), which drafts
40+
# the release: install.js and Scoop then 404 on releases/download/vX, and a
41+
# checkout of the tag fails (#332). So nothing downstream reads the tag: the
42+
# commit and the release are resolved independently of it, and a missing tag
43+
# or a drafted release is restored here, loudly, before anything publishes.
44+
release:
45+
name: Resolve the release and keep it published
46+
runs-on: ubuntu-latest
47+
permissions:
48+
contents: write
49+
outputs:
50+
commit: ${{ steps.resolve.outputs.commit }}
51+
release_id: ${{ steps.resolve.outputs.release_id }}
52+
steps:
53+
- name: Checkout main
54+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
55+
with:
56+
ref: main
57+
fetch-depth: 0
58+
59+
- name: Resolve the release commit and the GitHub release
60+
id: resolve
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
VERSION: ${{ github.event.inputs.version }}
64+
COMMIT: ${{ github.event.inputs.commit }}
65+
REPO: ${{ github.repository }}
66+
run: |
67+
set -euo pipefail
68+
export TAG="v${VERSION}"
69+
# An epoch squash re-anchors old tags and drops their "Release
70+
# version" commits, so the subject is only the fallback for a tag
71+
# pruned right after its release.
72+
SUBJECT="Release version ${VERSION}"
73+
[ -n "$COMMIT" ] || COMMIT=$(git rev-parse -q --verify "refs/tags/${TAG}^{commit}" || true)
74+
[ -n "$COMMIT" ] || COMMIT=$(git log origin/main --format='%H%x09%s' \
75+
| awk -F'\t' -v s="$SUBJECT" '$2 == s && !n++ { print $1 }')
76+
[ -n "$COMMIT" ] || { echo "no ${TAG} tag and no '${SUBJECT}' commit on main"; exit 1; }
77+
COMMIT=$(git rev-parse --verify "${COMMIT}^{commit}")
78+
git merge-base --is-ancestor "$COMMIT" origin/main \
79+
|| { echo "${COMMIT} is not on main"; exit 1; }
80+
RELEASE_ID=$(gh api "repos/${REPO}/releases?per_page=100" \
81+
--jq '[.[] | select(.tag_name == env.TAG or .name == "Release " + env.VERSION)][0].id // empty')
82+
[ -n "$RELEASE_ID" ] || { echo "no GitHub release for ${TAG}"; exit 1; }
83+
echo "commit=${COMMIT}" >> "$GITHUB_OUTPUT"
84+
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
85+
echo "${TAG}: commit ${COMMIT}, release ${RELEASE_ID}"
86+
87+
- name: Restore a pruned tag and a drafted release
88+
env:
89+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90+
VERSION: ${{ github.event.inputs.version }}
91+
COMMIT: ${{ steps.resolve.outputs.commit }}
92+
RELEASE_ID: ${{ steps.resolve.outputs.release_id }}
93+
REPO: ${{ github.repository }}
94+
run: |
95+
set -euo pipefail
96+
TAG="v${VERSION}"
97+
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null; then
98+
[ "$(git rev-parse "${TAG}^{commit}")" = "$COMMIT" ] \
99+
|| { echo "${TAG} points at $(git rev-parse "${TAG}^{commit}"), not ${COMMIT}"; exit 1; }
100+
else
101+
git config user.name "github-actions[bot]"
102+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
103+
git tag -a "$TAG" -m "Release version ${VERSION}" "$COMMIT"
104+
git push origin "refs/tags/${TAG}"
105+
echo "::warning::${TAG} was missing on GitHub; recreated at ${COMMIT}. Push it to Forgejo too."
106+
fi
107+
if [ "$(gh api "repos/${REPO}/releases/${RELEASE_ID}" --jq .draft)" = true ]; then
108+
gh api -X PATCH "repos/${REPO}/releases/${RELEASE_ID}" \
109+
-f tag_name="$TAG" -F draft=false -f make_latest=legacy >/dev/null
110+
echo "::warning::release ${TAG} was a draft; published it again"
111+
fi
112+
[ "$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .id)" = "$RELEASE_ID" ]
113+
34114
npm:
35115
name: Publish npm wrapper
116+
needs: release
36117
if: github.event.inputs.publish_npm == 'true'
37118
runs-on: ubuntu-latest
38119
permissions:
39120
contents: read
40121
id-token: write
41122
steps:
42-
- name: Checkout release tag
123+
- name: Checkout release commit
43124
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
44125
with:
45-
ref: v${{ github.event.inputs.version }}
126+
ref: ${{ needs.release.outputs.commit }}
46127

47128
- name: Download release binaries
48129
env:
49130
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50-
VERSION: ${{ github.event.inputs.version }}
131+
RELEASE_ID: ${{ needs.release.outputs.release_id }}
51132
run: |
133+
set -euo pipefail
52134
mkdir -p release-binaries
53-
gh release download "v${VERSION}" \
54-
--repo "${GITHUB_REPOSITORY}" \
55-
--dir release-binaries \
56-
--pattern 'diffctx-*-*.tar.gz' --pattern 'diffctx-*-*.zip'
135+
gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
136+
--jq '.assets[] | select(.name | test("^diffctx-.+-.+[.](tar[.]gz|zip)$")) | "\(.id) \(.name)"' \
137+
| while read -r id name; do
138+
gh api -H 'Accept: application/octet-stream' \
139+
"repos/${GITHUB_REPOSITORY}/releases/assets/${id}" > "release-binaries/${name}"
140+
done
57141
ls -la release-binaries
58142
59143
- name: Set up Python

‎.github/workflows/sensitivity-check.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
steps:
3333
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3434

35-
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
35+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
3636
with:
3737
python-version: '3.12'
3838
enable-cache: true

‎.pre-commit-config.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ repos:
140140
# LINTING (Ruff for speed + correctness)
141141
# ============================================================================
142142
- repo: https://github.com/astral-sh/ruff-pre-commit
143-
rev: v0.16.8
143+
rev: v0.16.10
144144
hooks:
145145
- id: ruff
146146
name: ruff (correctness-focused checks)
@@ -227,7 +227,7 @@ repos:
227227
# CODE DUPLICATION DETECTION
228228
# ============================================================================
229229
- repo: https://github.com/PyCQA/pylint
230-
rev: v4.0.8
230+
rev: v4.0.10
231231
hooks:
232232
- id: pylint
233233
name: pylint (duplicate code detection)

0 commit comments

Comments
 (0)