call-summary-schema-version.test.ts pins INCREMENTAL_SCHEMA_VERSION as a
literal per bump, documenting the reuse-gate boundary for each version.
Update the "current" expectation to 11 and add the v10 pre-current case,
matching the v7/v8/v9/v10 precedent already in the file.
RUST_SCOPE_QUERY gained a function_signature_item capture, shifting the
capture fingerprint for every bench fixture with a required trait method.
Verified: node --import tsx bench/scope-capture/measure.mjs --check now
passes across all 14 languages (rust scaling 1.036 < 1.5 budget).
Addresses gitnexus-review-agent findings on PR #2608:
- MED: on a partial apply (a file's write throws), drop that file's edits
from total_edits/graph_edits/text_search_edits/changes so the reported
result describes what actually reached disk, not what was attempted. The
comprehensive enumeration otherwise let a failing file contribute its
entire line count as phantom 'applied' edits. failed_files still names
every dropped file. Counts are now derived once from the reported set.
- MED: hoist the word-boundary regexes out of the per-line loop (one compile
each instead of one per line), reused by the apply loop.
- LOW: apply loop reuses escapedOldName instead of recomputing the escape
formula inline (removes a preview/apply drift risk).
- Soften the in-code comment: enumeration gives per-call preview/apply
consistency; the pre-existing two-read TOCTOU (external write between
preview and apply) is out of scope and noted, not newly introduced.
Tests: add a mixed graph-ref + text_search multi-file case (asserts per-file
confidence and the never-downgrade guard, via a stubbed rg), and a
partial-write-failure case (asserts only landed files are reported). Assert
concrete graph_edits/text_search_edits splits, not just their sum.
* fix(eval): bind the resolved node to a fresh sandbox path, not one under /usr
#2607 bound the resolved `node` to /usr/local/bin/node, but that path lives
inside the /usr tree that _runtime_mount_args already read-only-binds
wholesale. The second real workflow_dispatch run on the self-hosted runner
(https://github.com/abhigyanpatwari/GitNexus/actions/runs/29840270554)
failed immediately in the bubblewrap preflight: "bwrap: Can't create file
at /usr/local/bin/node: Read-only file system" -- bwrap can't create a new
mount-point file inside a tree it already bound read-only when the real
path doesn't already exist there on the host, which is exactly the
self-hosted case this bind exists to fix.
Introduces SANDBOX_NODE (/opt/claude/node), a fresh path outside every
tree _runtime_mount_args binds, following the same pattern SANDBOX_CLAUDE
and SANDBOX_PYTHON3 already use. Updates the two real call sites
(sanitized_graph.py, runner_sessions.py) to use the constant instead of
the hardcoded literal, so the fix can't drift out of sync with itself
again, and re-exports it from runner.py alongside the other SANDBOX_*
names for the real-bwrap tests that reference it directly.
Adds a real-bwrap test (gated behind GITNEXUS_REQUIRE_BWRAP_CANARY, same
as the existing ones) that copies a real node binary to a path outside
every bound tree and actually launches bwrap against it -- an
argv-construction test alone can't catch a bwrap-level "Read-only file
system" error, only a real invocation can, and that's exactly the gap
that let #2607's version of this fix through review looking correct.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(eval): don't let the new real-bwrap test's node-mock break bwrap's own resolution
CI caught this immediately: the new test_real_bubblewrap_runs_node_from_outside_the_bound_trees
monkeypatched shutil.which to return None for anything but "node", but
prepare_sandbox's own bwrap/claude resolution (_resolve_executable) goes
through shutil.which too -- so the test broke bwrap discovery before the
sandbox it's supposed to exercise could even be built ("SandboxError:
required executable is unavailable: bwrap").
Delegate to the real shutil.which for every other name instead of
blanket-returning None.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
RUST_SCOPE_QUERY gained a function_signature_item capture (previous commit)
so abstract trait methods can now dispatch a CALLS edge through a &dyn
Trait receiver. The incremental write set only covers changed files, so a
top-up against a pre-v11 index would keep silently missing these edges for
every unchanged Rust trait file — same contract as v7/v10; force a full
re-analyze instead.
Expected drift from the query.ts change: abstract trait methods now emit a
scope + declaration capture, shifting captureGroups/digest for every rust-*
fixture containing a trait with a required (bodyless) method.
New minimal fixture (single trait + impl + &dyn Trait call site, no other
same-named callers) proves the dyn-dispatch CALLS edge discriminates: fails
against the pre-fix source (0 edges) and passes against the two preceding
commits' fix (exactly 1 edge, verified via the CLI analyze pipeline against
a standalone repo).
The existing rust-abstract-dispatch fixture was NOT extended for this,
deliberately: it already has other callers referencing the same method
names (process()'s repo.find()/save()/count()), and an existing resolution
fallback picks those up via simple-name matching regardless of receiver
type — masking this specific defect in the in-process test-pipeline path.
A dedicated, single-caller fixture keeps the regression test load-bearing.
Surfaced by the first real workflow_dispatch run on the self-hosted runner
(https://github.com/abhigyanpatwari/GitNexus/actions/runs/29836411744):
every session failed with error_kind infra-error, error_detail "bwrap:
execvp /usr/local/bin/node: No such file or directory", tripping the
outage-streak breaker after 5 consecutive failures.
sanitized_graph.py and runner_sessions.py invoke the sandboxed graph CLI
at the fixed path /usr/local/bin/node. _runtime_mount_args only binds
/usr, /bin, /lib, /lib64 wholesale, so that path resolves correctly when
node happens to live under /usr/local/bin on the host -- true on
GitHub-hosted runner images, but not on a self-hosted runner, where
actions/setup-node installs into its own tool-cache directory instead
(outside all four bound trees, so invisible to the sandbox regardless of
what PATH says on the host).
Fix lives entirely in the mount construction: resolve `node` via
shutil.which (correctly picks up wherever actions/setup-node put it,
since its tool-cache dir is already on PATH by the time this runs) and
bind it read-only to the same fixed sandbox path the two call sites
already expect. Neither call site needed to change. Backward compatible
with GitHub-hosted runners, where this resolves to the same path and
binds a harmless no-op self-mount.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
rename() reported total_edits from a partial enumeration (definition line
only, one-edit-per-graph-file then break, and text search that skipped any
file already covered by the graph) while the apply step does a whole-file
\boldName\b global replace on every touched file. When a private symbol's
definition and all its call sites live in one file, only the definition line
was reported (total_edits: 1) even though apply rewrote every occurrence, in
both dry-run and apply.
Rebuild changes/total_edits/graph_edits/text_search_edits from one file set:
classify each file to rewrite (definition + graph refs = graph confidence;
rg-only files = text_search, never downgrading a graph file), then enumerate
every matching line per file with apply's exact escaped global regex. The
reported edit list now equals what apply writes. Apply behavior is unchanged.
Adds a regression test reproducing the issue's single-file Rust case (def +
3 same-file call sites, empty graph): total_edits is 4 in both dry-run and
apply, and equals the replacements that land on disk.
fn foo(&self) -> T; (no body) parses as function_signature_item, a grammar
node distinct from function_item that RUST_SCOPE_QUERY never captured. An
abstract trait method therefore had no Function scope and no declaration,
so populateClassOwnedMembers never wired its ownerId to the trait's Class
scope — invisible to the CALLS-edge receiver-bound resolution pass even
after a receiver's type resolves to the trait correctly.
Together with the previous commit's dyn-stripping fix, a call through a
&dyn Trait parameter now emits a CALLS edge to the trait's method (#2604).
normalizeRustTypeName/normalizeRustReturnType stripped reference sigils,
pointer sigils, and smart-pointer wrappers but never the `dyn` keyword, so a
`&dyn Trait`-typed receiver normalized to the literal string "dyn Trait"
instead of "Trait" — an unmatchable name that silently broke every
downstream receiver-type lookup for trait-object dispatch.
Part of the #2604 fix (root cause has a second, independent half: abstract
trait methods are invisible to scope resolution until function_signature_item
is captured — next commit).
Two gitnexus-review-agent findings on PR #2602:
- MEDIUM: the bodied-constant MRO-to-host-enum path (a qualified call to an
inherited, non-overridden enum method) was claimed in a comment but never
tested. Add EnumConst.A.log() -> EnumConst.log#0, exercising E$N's
@reference.inherits MRO arm end to end.
- LOW: `bodiedName ?? hostEnum` conflated "body-less" with "name synthesis
failed on a bodied constant" (reachable only on malformed/error-recovery
trees), silently binding an overriding constant's receiver to the host
enum — a wrong edge instead of no edge. Switch to `isBodied ? bodiedName :
hostEnum` so a bodied constant binds ONLY to its E$N class, mirroring the
object_creation_expression branch's skip-on-synthesis-failure. Verified
output-neutral on the well-formed bench corpus.
Rebaseline the java scope-capture fingerprint (a822cef9 -> d04298a9): the
bench corpus IS test/fixtures/lang-resolution, so the new dispatchInherited
fixture method shifts it (+6 capture groups); the logic change contributes
nothing (confirmed by isolating the fixture-only fingerprint). java.test.ts
242 passed; measure.mjs --check PASS (14 languages); tsc/prettier/eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(eval): move skill-evolution to a self-hosted runner and fix the sandbox's Python 3 trust gap
GitHub-hosted runners hard-cap job execution at 6 hours, which is too
short once a benchmark session actually invokes Skill/MCP tools for
real (the --bare fix in #2584 means sessions no longer no-op). Move the
job onto a self-hosted runner (5-day cap instead) and document the
activation step in the workflow's own checklist.
Validating the self-hosted run surfaced a real bug: gitnexus-plan
sessions inside the bwrap sandbox failed with "planning must create or
modify exactly one plan artifact; observed 0". Root cause:
evidence-provenance.mjs's atomic plan-writer only trusts a Python 3
binary owned by root or by the current process. Inside this
--unshare-user sandbox only the calling uid is mapped (root isn't), so
the real, root-owned /usr/bin/python3 surfaces as the kernel's overflow
uid and gets correctly refused as untrusted. Fix: provision a small,
self-owned wrapper script (same pattern already used for
shell-prefix) that execs the real interpreter, so the sandbox has a
Python 3 candidate the existing trust check can actually accept --
without touching that security-sensitive validation logic at all.
Also add visibility so this class of failure isn't quiet next time:
report.md now shows why each row failed (error_kinds), not just
resolved 0/1, and the benchmark now exits non-zero when an incumbent
arm -- the currently-shipped skill -- resolves zero across every task,
since that reads as a broken harness rather than a normal candidate
miss.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(eval): close the broken_incumbent_arms zero-valid-runs gap; document runner exposure tradeoff
Addresses the two MEDIUM findings from the gitnexus-review-agent on this PR
(https://github.com/abhigyanpatwari/GitNexus/pull/2600#issuecomment-5033363096).
broken_incumbent_arms required valid_runs > 0 before flagging an incumbent,
so an incumbent that fails every run with an excluded-but-non-systemic
error_kind (e.g. evidence-unverified, which the outage-streak breaker
explicitly resets on rather than accumulates) never accumulated a single
valid run and sailed through silently -- the exact quiet no-promotion
outcome this guard exists to catch, and arguably worse than the
some-runs-resolved-zero case since here nothing completed at all.
aggregate() never marks an excluded/unverifiable row resolved=True, so
dropping the valid_runs requirement and checking resolved == 0 alone
correctly covers both cases. Added a test for exactly this all-excluded
scenario, which none of the existing three did.
Updated the workflow's own activation checklist to reflect what's actually
true now (the gitnexus-evolution environment's branch policy and the
self-hosted runner are both live, codified in infra/gitnexus-evolution/ in
a companion PR) and documented the exposure-window tradeoff the review
flagged: the runner is stopped between runs but not destroyed/recreated per
run, so it isn't fully ephemeral. Stopping already bounds the exposure
window to the job's own runtime on one day out of seven; full per-job
ephemeral provisioning is a deliberate non-goal for a job that runs at
most weekly, revisit if that changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(eval): remove public infra/ pointers from the activation checklist
PR #2603 (the Terraform codification this checklist pointed to) got closed
-- publishing the exact IAM roles, security group rules, and self-hosted
runner topology for a real, live AWS account isn't safe to do in a public
repo, even with no literal secrets or resource IDs in the diff. The
underlying AWS/GitHub setup is unaffected and still documented privately;
this just removes the now-dangling references to a directory that won't
exist in this repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* ci(actionlint): register the gitnexus-evolution self-hosted runner label
actionlint rejected `runs-on: [self-hosted, linux, x64, gitnexus-evolution]`
in gitnexus-skill-evolution.yml because it can't discover custom runner
labels. Register it in .github/actionlint.yaml so the Workflow Lint check
passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The enum-constant receiver-dispatch fix adds one @type-binding.* capture
per enum constant, so the java scope-capture fingerprint shifts
(85fc7af9 -> a822cef9). Pure capture-additive drift; no bench fixtures
added; scaling 1.024 < 1.5 budget. Verified `measure.mjs --check` passes
for all 14 languages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Calling a method on an enum-constant receiver (E.CONST.method()) emitted
no CALLS edge. The receiver "E.CONST" is a two-segment compound receiver;
resolveCompoundReceiverClass walks each dotted segment via the owning
class scope's typeBindings map, but enum constants had no typeBinding, so
the constant segment dead-ended and no target was ever resolved.
#2555/#2558 gave bodied constants a first-class synthesized E$N class with
an MRO that includes the host enum; this is the receiver-side follow-up.
synthesizeJavaAnonymousClassDeclarations now emits a class-scope
typeBinding for every enum constant's simple name -> its E$N class (bodied)
or the host enum itself (body-less), reusing the exact mechanism a field
declaration uses. The generic compound-receiver chain walk then resolves
E.CONST.method() with no change to any shared scope-resolution code.
Bodied dispatch (EnumConst.A.hook() -> EnumConst$1.hook#0) and body-less
inherited dispatch (Plain.A.m() -> Plain.m#0) are covered by new tests in
the existing java-enum-constant-body fixture; both were verified to fail
against the pre-fix tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverses the prior convention: gitnexus-plan/gitnexus-work plan documents
under docs/plans/ are working artifacts and no longer travel with the PR.
Drops the require-node-22.18 plan doc from tracking; the .gitignore now
ignores all of docs/. The workflow_bench snapshot features scan the
filesystem, not git-tracked status, so they are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test_eval_ci_uses_locked_uv_and_blocking_native_containment_jobs pins the
eval-containment-linux job's setup-node version; move it in lockstep with
the ci-tests.yml pin bumped to the 22.18 floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The module.registerHooks compat seam and the onnxruntime resolvers cited
the old '>=22.0.0' floor as the reason their sub-22.15 fallback was
reachable. With the floor now ^22.18.0 || >=24.11.0 (all >=22.15), every
supported runtime exposes the API; the fallback stays as defensive
handling for below-floor runtimes (engines is advisory, not
engine-strict). Comments only - no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the supported minimum raised to Node 22.18, retarget every lane and
pinned runtime that sat at a lower version so nothing builds or runs the
package on an unsupported (EBADENGINE-warning) Node:
- ci-tests.yml: node-floor-compat 22.14 -> 22.18.0 (name, comment, pin,
version assertion) so the floor gate guards the new minimum; its #2372
registerHooks failure mode cannot recur above 22.15. Containment-canary
pin 22.16.0 -> 22.18.0.
- gitnexus-review-agent.yml + the pinned review/canary runtime: the
reproducible runtime is version-locked in lockstep across
.github/{gitnexus-review-runtime,claude-canary-runtime}/package.json and
their lockfiles (engines), the workflow's node-version, its two
'node --version = v22.18.0' assertions, the lockfile-engines guard, and
NODE_VERSION. Moved all of them 22.16.0 -> 22.18.0.
- gitnexus-skill-evolution.yml: pinned runtime 22.16.0 -> 22.18.0.
- CONTRIBUTING.md prerequisite floor updated.
- review-agent-workflow.test.ts, which enforces the runtime lock, updated
to expect 22.18.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Babel 8 (devDep for the bench mutation oracle, pulled in by dependabot
previous floor (>=22.0.0), so every dev install on Node <22.18 emitted
nine EBADENGINE warnings. Rather than pin Babel back to 7, adopt Node
22.18+ as the supported minimum: set engines to ^22.18.0 || >=24.11.0,
matching Babel 8 exactly so the warnings resolve honestly with no
dependabot ignore needed.
@types/uuid@11 is a deprecated stub - uuid@14 ships its own types and no
tsconfig references it. Lockfile edited by hand (engines + @types/uuid
entry) to preserve the libc platform metadata a newer npm wrote;
verified consistent via npm ci (exit 0).
BREAKING CHANGE: the gitnexus package now requires Node ^22.18.0 || >=24.11.0
(previously >=22.0.0). Node 22.0-22.17 are no longer supported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Anchor isBenignDropFtsIndexError to the START of the message
(startsWith, not includes) so a future genuine failure that merely
mentions "Binder exception" or "Catalog exception" mid-message can't
be misclassified as benign. New test proves the old substring match
would have swallowed such a message.
- incremental-fts-drop-ordering.test.ts: probe FTS availability once in
beforeAll and skip VISIBLY via ctx.skip() in beforeEach (matching the
withTestLbugDB/lbug-vector-extension convention) instead of a silent
console.warn+return inside the test body, which reported a false pass
with zero coverage of the ordering invariant when FTS was unavailable.
The post-first-run FTS-index-built check is now a hard assertion
instead of a second soft skip, since the beforeEach gate already
proved the extension loads.
CI caught this: adding the record_declaration capture legitimately
changes the pinned java capture fingerprint, same as every prior
capture-behavior change to this language (#2550, #2555). Rebaselined
following the established _rebaselined_* precedent; scaling ratio
1.059 stays well within the 1.5 budget.
Fixes#2589: incremental analyze intermittently crashed with "FTS
index 'file_fts' is inconsistent: term is missing during delete" after
markdown-only commits, and --repair-fts also failed in that state.
deleteNodesForFiles' batched DETACH DELETE ran against tables that
still carried the FTS index built at the end of the PREVIOUS analyze
run -- createSearchFTSIndexes only drops+rebuilds every index in Phase
3, well after that delete already ran. LadybugDB's FTS extension is
not proven to survive DML against an indexed table (its own docs never
demonstrate the sequence). Call the new dropSearchFTSIndexes() up front
in the non-escalated incremental branch, before deleteNodesForFiles --
Phase 3 still rebuilds every index from the final row set regardless.
New end-to-end test drives a real runFullAnalysis full+incremental
cycle and confirms it fails without this change (file_fts and 50
sibling indexes still present at delete time) and passes with it.