Commit graph

145 commits

Author SHA1 Message Date
dependabot[bot]
3e38cd0eb5
chore(deps): bump docker/setup-qemu-action from 4.1.0 to 4.2.0 (#2408)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](06116385d9...96fe6ef7f3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 06:53:19 +01:00
dependabot[bot]
1d5ffd55c8
chore(deps): bump actions/attest-build-provenance from 4.1.0 to 4.1.1 (#2406)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](a2bbfa2537...0f67c3f485)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 06:51:04 +01:00
dependabot[bot]
4c7b4c95d8
chore(deps): bump docker/build-push-action from 7.2.0 to 7.3.0 (#2404)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](f9f3042f7e...53b7df96c9)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 06:50:48 +01:00
dependabot[bot]
62d90786a6
chore(deps): bump raven-actions/actionlint from 2.1.2 to 2.2.0 (#2399)
Bumps [raven-actions/actionlint](https://github.com/raven-actions/actionlint) from 2.1.2 to 2.2.0.
- [Release notes](https://github.com/raven-actions/actionlint/releases)
- [Commits](205b530c5d...3d39aea434)

---
updated-dependencies:
- dependency-name: raven-actions/actionlint
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 06:49:11 +01:00
dependabot[bot]
d287f98e0c
chore(deps): bump release-drafter/release-drafter from 7.4.0 to 7.5.1 (#2398)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.4.0 to 7.5.1.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](ed4bc48ec9...4d75298e00)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 06:48:55 +01:00
azizur100389
f236be05e0
feat: gate Icebug community engine prototype (#2376) 2026-07-09 05:43:49 +01:00
Gergő Magyar
8402963198
fix(ci): shard platform-sensitive matrix + spawn built CLI to fix Windows cross-platform timeout (#2394)
* fix(ci): shard platform-sensitive matrix + spawn built CLI to fix Windows cross-platform timeout

The `windows-latest (platform-sensitive)` job was hitting its 15-min internal
vitest watchdog in run-cross-platform.ts. It's cumulative slowness, not a hang:
the fixed 72-file suite is dominated by ~50 CLI/worker process spawns, and
Windows is ~5x slower than macOS at process startup (macOS ran the same set in
~3min of tests). Two complementary changes bring it back under the watchdog with
headroom, without touching any test assertion:

- Shard the platform-sensitive matrix (windows/macos × shard [1,2]) and forward
  `--shard=i/2` through run-cross-platform.ts to vitest, which partitions the
  fixed file list deterministically (sha1, equal file-count) — halving each
  runner. macOS/Ubuntu were already under budget.
- New test/helpers/cli-entry.ts (`CLI_SPAWN_PREFIX`): spawn the built
  `dist/cli/index.js` when `GITNEXUS_E2E_CLI=dist` (set on the cross-platform job,
  which already builds) instead of `node --import tsx src/cli/index.ts`, which
  re-transpiles the whole CLI on every spawn. Defaults to tsx-on-source so local
  runs always reflect current source; `GITNEXUS_E2E_CLI=dist` on an unbuilt tree
  throws an actionable "run npm run build" error. dist is opt-in only — never
  inferred from a generic `CI` env — so an ambient `CI=1` can't silently run a
  stale build. Converted 8 spawn-based e2e suites; added test/unit/cli-entry.test.ts.

The Ubuntu coverage job leaves `GITNEXUS_E2E_CLI` unset, so the tsx-on-source path
stays exercised in CI too (both entry points covered).

Measured on Linux: cli-limit-e2e 121.5s→91s, cli-e2e 289s→217s (~25%); larger on
Windows where the transpile is a bigger share of each spawn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ci): derive platform-sensitive shard count from one source (#2394)

The shard total was hardcoded in three coupled, unenforced places (matrix
length, job-name suffix, --shard denominator); editing one without the others
silently dropped a shard's tests with green CI. Add a checkout-free shard-plan
job whose single TOTAL generates both the shard index list (consumed via
fromJSON) and the /N denominator (job name + --shard arg), so they cannot
drift. Asserts TOTAL>=1 to rule out an empty-matrix silent skip. No behavior
change — still 2 shards per OS.

Addresses PR #2394 tri-review finding F2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): 3 shards for real Windows headroom + honest sharding comments (#2394)

vitest shards by file COUNT, not runtime, so the heaviest spawn suites cluster
into one shard: live CI showed Windows shard 1/2 at 12m12s (~81% of the 15-min
watchdog) vs shard 2/2 at 3m0s. The old comments claimed "comfortable/generous
headroom", which the count-based split doesn't deliver at 2 shards. Bump TOTAL
to 3 (one line, single source) so even the busiest Windows shard clears the
watchdog, and reword the comments to describe count-based (not time-based)
sharding.

Addresses PR #2394 tri-review finding F1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ci): extract testable parseShardArg from run-cross-platform (#2394)

The --shard parse/forward glue had no unit test. Extract it into a pure
scripts/shard-arg.ts (mirroring the computeSpawnPrefix extraction precedent) so
the branch logic is lockable without the script's top-level execFileSync, and
add test/unit/shard-arg.test.ts (absent -> undefined, valid token -> passed
through, found amid other args). Behavior unchanged; U4 adds the malformed
fail-loud on top.

Addresses PR #2394 tri-review finding F3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): fail loud on a malformed --shard arg (#2394)

A shard-shaped-but-malformed arg (--shard=1, --shard, --shard=abc) was silently
ignored, dropping the shard flag so both legs ran the full unsharded ~50-spawn
suite — re-arming the Windows watchdog timeout with no signal. parseShardArg now
throws an actionable error on any --shard/--shard=… arg that fails the strict
regex (unrelated flags like --shardx= pass through), and the call site in
run-cross-platform.ts catches it into console.error + exit 1, kept outside the
execFileSync try so the message isn't swallowed by that catch's watchdog-only
branch.

Addresses PR #2394 tri-review finding F4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): fail loud on an unknown GITNEXUS_E2E_CLI value (#2394)

computeSpawnPrefix silently degraded any unknown GITNEXUS_E2E_CLI value to
tsx-on-source, so a typo (e.g. `dsit`) would make CI believe it tests the dist
entry point while actually running src. Throw on any value other than
'dist'/'src'/unset (the safe tsx default is preserved for unset/''/'src', so it
still never selects dist without an explicit opt-in). Flip the unknown-mode unit
test to assert the throw and add the missing {mode:undefined, distExists:true}
case. Only ci-tests.yml sets the var (=dist), so no existing suite is affected.

Addresses PR #2394 tri-review findings minor-a/b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ci): run cli-entry.test.ts on the cross-platform matrix (#2394)

cli-entry.test.ts resolves CLI_SPAWN_PREFIX from a real path, and its last
assertion (cli[/\\]index) has a Windows backslash branch that only Ubuntu
exercised. Register it in PLATFORM_LOGIC so it runs on the Windows/macOS matrix
too. (shard-arg.test.ts stays out — pure string logic, OS-independent.) List
grows 73 -> 74; the generated shard matrix keeps coverage complete.

Addresses PR #2394 tri-review finding minor-c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(test): share tsxLoaderUrl(), dedup the last tsx-loader boilerplate (#2394)

bridge-cache-reopen.test.ts carried its own copy of the tsx-loader-resolution
boilerplate (createRequire -> resolve('tsx/package.json') -> pathToFileURL) —
the one site the PR's CLI_SPAWN_PREFIX migration didn't cover (it spawns a seed
script, not the CLI). Export the existing tsxLoaderUrl() from cli-entry.ts and
reuse it here; the resolved loader URL is byte-identical.

Addresses PR #2394 tri-review finding minor-d.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): make skipUnlessFtsAvailable install FTS on miss so shards are self-sufficient (#2394)

Sharding the platform-sensitive suite into 3 exposed a latent test-isolation
bug: load-only FTS primitives (test/integration/lbug-core-adapter.test.ts) only
passed because a sibling installer test happened to co-locate in the same shard
and install FTS into the shared ~/.lbdb first. At 3 shards, lbug-core-adapter
landed in a shard with no installer sibling, so its load-only loadFTSExtension()
failed deterministically on macOS+Windows shard 2/3 under GITNEXUS_REQUIRE_FTS=1.

Make the gate self-sufficient: on a load-only miss under REQUIRE_FTS, install
FTS with `auto` (LOAD-first, then one bounded network INSTALL) before treating
it as a hard failure — mirroring withTestIndexedDB. A pre-installed extension
still costs no network (auto is LOAD-first); offline/local runs (no env var)
still skip gracefully. Verified: with a fresh HOME (no pre-installed FTS) +
REQUIRE_FTS=1, lbug-core-adapter now passes 15/15 (previously threw).

Addresses the 3-shard CI failure surfaced while validating PR #2394's F1 fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: warm-cache the LadybugDB FTS extension across platform shards (#2394)

Follow-up to the FTS self-install fix: cache ~/.lbdb/extension per OS + lockfile
so a warm run skips the network install entirely and the parallel shards share
one download across runs. Pure reliability/speed — on a cache miss the tests
still self-install FTS on demand (test/helpers/fts-availability.ts), so this is
never a correctness dependency, just a way to cut the network-install surface
that made the sharded FTS tests flaky. Keyed by lockfile hash (a LadybugDB
version bump re-installs); per-OS since the extension is a native binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): pass shard via env to clear zizmor template-injection (#2394)

Interpolating ${{ matrix.shard }} (now sourced from the shard-plan job output)
directly into the run: shell tripped zizmor's template-injection audit
(code-scanning alert #824, ci-tests.yml:147). Move the value into a SHARD env
var — assigned via ${{ }} but referenced as "$SHARD" in the shell, which is not
an injection sink — and set shell: bash so the expansion is uniform across the
windows + macOS matrix (the default run shell is pwsh on Windows, where $SHARD
would be empty and trip the new malformed-shard fail-loud). Verified locally
with zizmor: the :147 template-injection finding is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: shard the ubuntu coverage job and merge blobs before the threshold gate (#2394)

The coverage job ran the full suite unsharded (~16 min). Shard it like the
cross-platform matrix, then merge the per-shard coverage before enforcing the
threshold gate:

- shard-plan now also single-sources the coverage shard count (cov_total /
  cov_shards), so the coverage matrix + /N denominator can't drift.
- The `tests` job becomes a coverage shard matrix: each shard runs
  `vitest run --shard --coverage --reporter=blob` with thresholds forced to 0
  (a single shard's partial coverage can never meet the gate) and uploads its
  blob. FTS self-installs per shard, so sharding the full suite is safe.
- New `coverage-merge` job (needs: tests) reduces the blobs with
  `vitest --mergeReports`, enforcing the REAL config thresholds on the combined
  ('new') coverage — this is the gate. It also emits the merged test-results.json
  and runs the unsharded web + docker suites, so the `test-reports` artifact
  keeps the exact shape ci-report.yml consumes for its base-branch ('baseline')
  vs new coverage delta.

The shard arg goes through a SHARD env var + shell: bash (no template-injection).
Validated locally: shard blobs write and merge into a coverage-summary.json +
merged test-results.json; the merge enforces thresholds on the union. CI Gate
still aggregates the coverage-merge result via the reusable-workflow call.

Note: the coverage check names change (ubuntu / coverage 1/3 … + merge) — update
any pinned branch-protection required checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): include hidden files when uploading the coverage blob (#2394)

The coverage shards write their blob to gitnexus/.vitest-reports/ (a dotdir).
actions/upload-artifact excludes hidden files by default, so the coverage-blob-*
artifacts uploaded empty — the merge job then downloaded 0 artifacts and
vitest --mergeReports failed with ENOENT scandir '.vitest-reports'. Set
include-hidden-files: true on the blob upload so the blobs actually ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): group shard-plan GITHUB_OUTPUT writes to satisfy shellcheck SC2129 (#2394)

Adding the coverage shard outputs (cov_shards/cov_total) made the shard-plan gen
step write four individual `>> "$GITHUB_OUTPUT"` redirects, which shellcheck
(run by the actionlint check) flags as SC2129. Group the echoes into a single
`{ …; } >> "$GITHUB_OUTPUT"` block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(test): cost-balanced shard sequencer to cut CPU contention (#2394)

vitest's default --shard hashes file paths and splits by file COUNT, which
clustered the spawn-heavy suites onto one runner (Windows platform shard 1 ran
~4x the others). Add a custom sequence.sequencer that overrides only shard()
and balances by estimated WORK instead:

- specWeight() weights the fileParallelism:false spawn-heavy suites (cli-e2e,
  lbug-db — already isolated to run sequentially) far above the parallel default
  files, plus file size as a cheap finer signal. Deterministic per checkout.
- assignShards() does greedy longest-processing-time bin-packing (heaviest file
  into the currently-lightest shard). The partition stays complete and disjoint
  — verified: on the 74-file cross-platform set the three shards weigh
  7611/7610/8064 (the sequential-heavy files spread ~7/7/8) with zero overlap and
  no file dropped, vs the hash split's count-only balance.

sort() is left to the base sequencer so project groupOrder / duration-cache
ordering is untouched. Pure logic split into shard-balance.ts with a unit test
locking the disjoint+complete, balance, and determinism properties.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): install + cache FTS up front on the coverage (and cross-platform) shards (#2394)

coverage 3/3 failed on extension-binary-real.test.ts: it uses the file-path FTS
gate (requireFtsResourceOrSkip), which resolves ~/.lbdb/extension at MODULE LOAD
and cannot self-install the way the load-path gate (skipUnlessFtsAvailable, U8)
does. The coverage job had no FTS cache and relied on an installer test running
first in the shard — the balancing sequencer reshuffled the shards and dropped
extension-binary-real into a shard with no installer, so FTS was absent.

Remove the ordering dependency: add scripts/ensure-fts.ts (init a throwaway lbug
db, loadFTSExtension with policy:auto → LOAD-first, INSTALL on miss) and run it
up front on every coverage AND cross-platform shard, after restoring the per-OS
FTS cache. The coverage job now shares that same cache key (it previously had
none — this is the "share the cached FTS with coverage" the failure pointed at).
Cold cache installs once; warm cache is a no-network load. Verified locally:
ensure-fts installs FTS into a fresh HOME and is a no-op when already present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 09:09:11 +01:00
Gergő Magyar
cdad478c96
fix: proxy-blocked installs survive onnxruntime-node postinstall and self-heal embeddings (#2370) (#2372)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-07-05 16:15:10 +01:00
dependabot[bot]
0087ce4fa1
chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#2352)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](b430933298...718ea10b13)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 07:12:17 +01:00
dependabot[bot]
4c8aceecc2
chore(deps): bump actions/cache from 5.0.5 to 6.1.0 (#2351)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](27d5ce7f10...55cc834586)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 07:11:57 +01:00
dependabot[bot]
0f9474cbf7
chore(deps): bump actions/setup-python from 6.2.0 to 6.3.0 (#2350)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a309ff8b42...ece7cb06ca)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 07:11:23 +01:00
Gergő Magyar
576e81442e
fix(search): index description field for FTS so doc comments are keyword-searchable (#2300)
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (push) Has been cancelled
* fix(search): index description column for FTS so doc comments are keyword-searchable

Closes #2299. descriptionExtractor (#2286) populates the `description`
column for every symbol table, but FTS only indexed name+content on 5
tables, so doc-comment keywords (Javadoc/KDoc/godoc/Rust ///) were
invisible to BM25 keyword search.

- Add `description` to the Function/Class/Method/Interface FTS indexes
  (File has no description column, left as name+content).
- Add FTS indexes for the remaining EMBEDDABLE_LABELS symbol tables
  (Struct, Enum, Trait, Impl, Macro, Namespace, Constructor, TypeAlias,
  Typedef, Const, Property, Record, Union, Static, Variable).
- createSearchFTSIndexes now drops-then-creates each index so the schema
  change reaches existing DBs on incremental re-analyze and --repair-fts
  (createFTSIndex is idempotent-by-name and would otherwise skip stale
  indexes).

Tests: fts-schema column-subset + coverage guards; drop-before-create
order; e2e doc-comment keyword search (Java class + Rust struct found by
description-only terms). bm25-search assertions derive from FTS_INDEXES.

* fix(review): apply autofix feedback

- Guard the --repair-fts path on FTS-extension availability before
  createSearchFTSIndexes drops-then-creates indexes (P1 regression:
  without the gate, an unavailable extension could drop existing indexes
  then fail to recreate them, leaving the DB index-less). Mirrors the
  analyze path's ftsAvailable gate and fails loudly first.
- Add a re-analyze upgrade integration test: seed an old name+content-only
  DB (no Struct index), run the real createSearchFTSIndexes(), and assert
  description keyword search + the previously un-indexed Struct now resolve.
  Proves drop-then-create upgrades a live stale index end-to-end.

* fix(ci): add loadFTSExtension to --repair-fts test mocks

The R3 review fix added a loadFTSExtension availability gate to the
--repair-fts path, but run-analyze-fts-repair.test.ts mocked the lbug
adapter without that export, so both repair tests threw `No
"loadFTSExtension" export`. Add loadFTSExtension to the two mocks
(returning true to preserve their original intent) and add a dedicated
test proving the guard fails loudly — and does NOT drop any index —
when the extension is unavailable.

* test(fts): run fts-description-search in the sequential lbug-db project

It was the only FTS-index-creating integration test left in the parallel
`default` vitest project; every other ftsIndexes-using test (search-core,
search-pool, augmentation, …) runs in the `lbug-db` project, which forces
fileParallelism: false to avoid LadybugDB native mmap file-lock conflicts
in parallel forks (Windows). Add it to the lbug-db include list and the
default exclude list to match the convention and remove the flake risk.

* test(ci): fail loudly when FTS extension is unavailable, never silently skip

FTS-dependent lbug integration suites (search-core, search-pool,
augmentation, fts-description-search, …) self-skip via ctx.skip() when the
LadybugDB FTS extension can't load, emitting only a console.warn while the
job stays green. That means a broken/missing FTS extension in CI would make
these integration tests silently vanish with no signal — false confidence.

withTestLbugDB now honors GITNEXUS_REQUIRE_FTS=1: when set and the extension
is unavailable, setup() throws instead of skipping, so the suite fails
loudly. The CI test jobs (ubuntu coverage + windows/macOS cross-platform)
set the flag; local/offline runs leave it unset and keep skipping
gracefully. (Verified the extension currently loads on all three runners,
so this is a guard against regression, not a behavior change today.)

* test(ci): run fts-description-search on macOS/Windows cross-platform jobs

The new FTS description-search suite was registered in the sequential
lbug-db vitest project (ubuntu/coverage) but absent from LBUG_NATIVE, so
the macOS/Windows platform-sensitive jobs (which run only the explicit
ALL_CROSS_PLATFORM allowlist via run-cross-platform.ts) never executed it.
The GITNEXUS_REQUIRE_FTS=1 hardening on those jobs guarded the old FTS
fixtures but not the new 20-index/description path. Add the suite to
LBUG_NATIVE so the new path is validated cross-platform too.

Refs #2299.

* fix(search): verify FTS indexes cover description, not just queryability

verifySearchFTSIndexes probed each index with QUERY_FTS_INDEX and treated
'queryable' as 'present'. A stale name+content-only index left on a
pre-#2299 DB stays queryable yet silently misses the description column, so
verification would pass green while doc-comment search stayed broken.

Switch to a single CALL SHOW_INDEXES() that exposes property_names per
index, and report an index as missing when it is absent OR does not cover
its configured columns. Return contract (string[] of table.indexName) is
unchanged, so both run-analyze.ts call sites are untouched. The per-index
string interpolation is gone, so the now-dead safeIdentifier helper is
removed.

The real caller of the live function in tests is bm25-search.test.ts (the
repair test mocks verifySearchFTSIndexes wholesale); its two probe-shaped
cases are rewritten to feed SHOW_INDEXES rows and now assert column
coverage, plus an absent-index case.

Refs #2299.

* test(search): assert description search via the public query surface

The #2299 integration suite only exercised the searchFTSFromLbug helper.
Add a third block that drives the public LocalBackend.callTool('query')
path — which resolves the repo via the registry and routes BM25 through the
pool adapter (a different connection context than the core-adapter helper) —
and asserts a description-only keyword returns the seeded class. Reuses the
existing description-only SEED and production FTS_INDEXES; partial-mocks
repo-manager so listRegisteredRepos points at the test DB while
cleanupOldKuzuFiles and the rest stay real.

Refs #2299.

* test(search): make lbug-core-adapter FTS gate honor GITNEXUS_REQUIRE_FTS

lbug-core-adapter.test.ts has its own per-test FTS gate (skipUnlessFtsAvailable)
that called ctx.skip() whenever the extension could not load — bypassing the
GITNEXUS_REQUIRE_FTS=1 hardening that withTestLbugDB already honors. Since this
file is in LBUG_NATIVE it runs on the ubuntu/macOS/windows jobs that all set
GITNEXUS_REQUIRE_FTS=1, so an FTS regression on a runner would have let these
FTS-primitive tests silently vanish from a green run — the exact gap #2299's
test-infra hardening set out to close.

Make the helper mirror withTestLbugDB: when GITNEXUS_REQUIRE_FTS=1 and the
extension is unavailable, throw (hard fail) instead of skipping. Offline/local
runs (no env var) still skip gracefully.

Refs #2299.
2026-06-25 14:21:44 +01:00
dependabot[bot]
5f667c32a3
chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2292)
* chore(deps): bump actions/checkout from 6.0.3 to 7.0.0

Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](df4cb1c069...9c091bb21b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* ci: set persist-credentials: false on read-only checkouts (zizmor artipacked)

Adds `persist-credentials: false` to the 9 checkout steps flagged by
zizmor's credential-persistence (artipacked) rule on PR #2292. All are
read-only CI/test/quality jobs that never use the git token afterward, so
not persisting it removes the leak surface. Checkouts that push (publish,
pr-autofix, commit-fork-prebuilds, etc.) keep credentials and are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:53:47 +01:00
dependabot[bot]
a05a1659bd
chore(deps): bump release-drafter/release-drafter from 7.3.1 to 7.4.0 (#2295)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.3.1 to 7.4.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](693d20e7c1...ed4bc48ec9)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 06:25:41 +01:00
glier
d27fd11c4b
fix(lang-kotlin): support fun interface extraction via tree-sitter-kotlin re-vendor (#2271)
* fix(lang-kotlin): support `fun interface` extraction via tree-sitter-kotlin re-vendor

Vendored tree-sitter-kotlin@0.3.8 (fwcd) parsed `fun interface Foo` as an
ERROR node and dropped the declaration plus its abstract method, so functional
(SAM) interfaces were never extracted. The fix landed upstream in
fwcd/tree-sitter-kotlin#169 (closes #87), merged to main 2025-04-25, but is not
in any npm release (latest tag 0.3.8; main is the unreleased 0.4.0).

Re-vendor the grammar from the unreleased fwcd main commit c8ac3d26:
- refresh src/{parser.c,scanner.c,node-types.json,tree_sitter/*.h} and
  bindings/node/index.js; bump the vendor version 0.3.8 -> 0.4.0; record the
  pinned SHA + rationale in _vendoredBy and the vendor README.
- switch the prebuild workflow's kotlin registry kind 'npm' -> 'vendored' (the
  fix is unreleased on npm, so prebuilds must build from the vendored C source,
  like swift/dart/proto).
- add a hold to .github/vendored-grammars.json so the weekly auto-update
  monitor does not strict-inequality-revert the pin to the broken npm 0.3.8
  (isNewer compares 0.3.8 != 0.4.0).
- add 3 regression tests + a fixture asserting fun interfaces extract as
  Interface nodes with their abstract methods, and that plain-interface
  heritage still resolves.

Existing KOTLIN_QUERIES need no change: the new grammar models `fun interface`
as a class_declaration with an "interface" keyword child (plus an extra "fun"
modifier child), which the existing interface rule already matches. Full Kotlin
suite green against the new grammar (300 unit/cfg/resolver + 233 integration).

NOTE: prebuilds/ are intentionally not in this commit. The version bump
auto-triggers .github/workflows/build-tree-sitter-prebuilds.yml, which
regenerates all 6 platform binaries from the vendored source in a separate PR.
Until that lands, CI loads the committed 0.3.8 prebuild, so the new kotlin
tests are red and the grammar change is inert at runtime. Merge the prebuild PR
first or together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ci): count kotlin's vendored hold as a 0.25-readiness blocker

The kotlin `hold` added in the previous commit makes the tree-sitter
upgrade-readiness report count it as a blocker — the report treats every
held vendored grammar as frozen below a runtime upgrade (same as the
intentionally-pinned tree-sitter-cpp and the ABI-held tree-sitter-c),
"in-range ABI or not". So the report's blocker count goes 2 -> 3.

Update the hardcoded count in
test_issue_update_summary_regex_matches_current_report (and the
_render_report docstring) accordingly — exactly as that test instructs:
"if a grammar is added/removed or a pin/hold changes, update the expected
counts". kotlin's ABI (14) is in range; the hold is what flags it, with the
reason recorded in .github/vendored-grammars.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ci): refresh kotlin baselines for the grammar bump

Two committed baselines pinned the pre-bump kotlin state and broke when the
grammar was re-vendored (0.3.8 -> 0.4.0):

- cli-commands.test.ts pinned the vendored kotlin package version at 0.3.8 ->
  update to 0.4.0.
- bench/scope-capture/baselines.json: the new kotlin-fun-interface fixture joins
  the lang-resolution/kotlin-* corpus AND the new grammar parses `fun interface`
  as a class_declaration (not an ERROR node), so the capture fingerprint drifts.
  Rebaselined to the NEW grammar's fingerprint (verified by building the vendored
  parser.c against tree-sitter@0.21.1 and running measure.mjs --check); scaling
  ~0.83 (linear).

Like the fun-interface integration tests, the scope-capture --check passes only
once the regenerated prebuilds land; until then CI loads the committed 0.3.8
binary, so it stays red.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(prebuilds): rebuild + commit grammar prebuilds into the PR on vendored-source change

build-tree-sitter-prebuilds.yml previously rebuilt a grammar's native prebuilds
only when its package.json VERSION bumped, and delivered them via a separate bot
PR. Now any change to the vendored grammar source re-cuts the prebuilds and they
ride into the same PR.

- Trigger on any build-affecting change under gitnexus/vendor/tree-sitter-*/**
  (parser.c, grammar.js, binding.gyp, scanner, bindings), not just version bumps.
  The prebuilds/ subtree is negated in the paths filter AND excluded from the
  guard's source diff, so the bot's own prebuild commit can never retrigger the
  workflow (no build -> commit -> build loop).
- The guard builds a grammar when its recorded version changed OR its vendored
  source changed vs the PR base.
- Same-repo PRs get the rebuilt prebuilds committed straight onto their own head
  branch (included in the SAME PR) via a non-force push that only adds a commit
  on top of head. Manual dispatch still opens a fresh chore/ PR; fork PRs stay
  artifacts-only (a bot cannot push into a fork branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(prebuilds): deliver rebuilt prebuilds to fork PRs via a trusted workflow_run stage

A fork PR's producer run has a read-only token and no secrets, so it can build and
validate the prebuilds but can't commit them. Add the safe two-stage handoff that
mirrors the pr-autofix producer/publish split.

- build-tree-sitter-prebuilds.yml (untrusted producer): on a fork PR, upload a
  pr-meta artifact (schema, pr_number, head_sha, head_ref, head_repo, base_repo)
  alongside the prebuild artifacts. Values flow through env + jq, never
  interpolated into a shell.
- commit-fork-prebuilds.yml (trusted, workflow_run): downloads ONLY the artifacts
  (never executes fork code — it checks out the pinned HEAD SHA solely to add
  files), allowlist-validates every metadata field, cross-checks identity against
  the workflow_run authority (head_sha / head_repo / pr_number, via
  commits/{sha}/pulls for forks), then pushes the prebuilds onto the fork head
  branch with --force-with-lease + http.extraheader auth. No PAT: this works when
  the contributor left "Allow edits by maintainers" on; on push failure it posts a
  sticky comment telling them to enable it or commit the downloaded artifacts.

zizmor: allowlist commit-fork-prebuilds.yml's workflow_run dangerous-trigger with
the documented mitigation, matching the existing ci-report / pr-autofix entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(vendor): rebuild tree-sitter-kotlin prebuilds for the re-vendored fun-interface grammar

The fun-interface re-vendor changed vendor/tree-sitter-kotlin source but left main's
old (0.3.8) prebuilds in place, so all 6 platform binaries were stale relative to the
new parser. Replace them with the freshly cross-built + ABI-validated binaries from
build-tree-sitter-prebuilds run 28010841458 — each .node was require()-loaded and
parsed a snippet on its target platform-arch before upload.

This is the manual equivalent of the commit-fork-prebuilds.yml delivery, which can't
run for this fork PR until it lands on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lang-kotlin): read extension-function receiverType from the re-vendored grammar's `receiver` field

The fun-interface re-vendor changed the kotlin AST: an extension function's
receiver is now a `receiver_type` exposed via a named `receiver` field, where the
old grammar emitted a bare user_type before the name. extractReceiverType only
matched the old shape, so receiverType came back null
(method-extraction.test.ts > Kotlin MethodExtractor > extracts receiverType).
Prefer the `receiver` field (unwrapping it), and keep the old child-scan — now
also recognizing `receiver_type` — as a fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-23 10:01:28 +01:00
Parafee41
7916c315f0
ci: fail tree-sitter summary parse drift (#2246)
* ci: fail tree-sitter summary parse drift

* docs(ci): cross-reference readiness regexes to their test mirror

The two report.match() literals in the upsert-issue github-script step are
duplicated as _ISSUE_READY_RE / _ISSUE_BLOCKER_RE in
test_check_tree_sitter_upgrade_readiness.py, and only the Python copy is
asserted against the rendered report. Since a stale regex now throws via
requireMatch (instead of the old silent '?' fallback), add a reciprocal
keep-in-sync note at the workflow site so a future prose edit can't desync
the JS literal from the asserted mirror undetected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): route read-phase network failures to fetch_failed, not a crash

npm_view_json and fetch_text caught only (URLError, HTTPError[, JSONDecodeError]).
urllib wraps connect-phase OSErrors into URLError, but a failure during
resp.read() AFTER urlopen returns (ConnectionResetError, ssl.SSLError,
socket.timeout, http.client.IncompleteRead) is not a URLError subclass — it
escaped the helper, crashed main(), and left stdout empty. main() is unguarded
(the only top-level except wraps just stdout.reconfigure), and the report print
is its last statement, so an empty report then makes the workflow's requireMatch
throw on a non-drift scheduled run.

Broaden both except tuples with OSError + http.client.IncompleteRead so a
transient mid-body network blip yields None, routing the grammar to the existing
fetch_failed blocker bucket (a complete report) — preserving the fail-loud intent
for real drift while removing the crash-to-empty-stdout path. JSONDecodeError
stays explicit (it is a ValueError, not an OSError). Adds read-phase regression
tests that fail on the old narrow tuple.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ci): document the regex-contract assertion counts

test_issue_update_summary_regex_matches_current_report asserts hardcoded
capture groups ("9","10") and "2" with no explanation. Document the
derivation from _render_report()'s mock corpus — 9 of 10 npm grammars Ready
(tree-sitter-cpp is the intentional pin), 2 blockers (pinned tree-sitter-cpp +
held vendored tree-sitter-c) — so a future grammar or pin change is an obvious
two-step update (mock + counts) rather than a mystery failure. Assertions
unchanged; comment only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: name _Resp method receivers 'self' to clear py/not-named-self

The _Resp stub inside _patch_urlopen named its method receivers
`self_inner`, which CodeQL flags as py/not-named-self (PEP 8) — three
alerts on this PR's merge ref (lines 230/233/236). _patch_urlopen is a
@staticmethod, so there is no outer `self` to collide with; rename the
receivers to the conventional `self`. Pure rename, no behavior change.

All 25 tests in test_check_tree_sitter_upgrade_readiness still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:45:09 +01:00
Gergő Magyar
78b4077d8a
feat(impact): opt-in PDG-backed impact mode - statement + inter-procedural slicing, resolved-callee-id soundness, mutation-oracle validated (#2227)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
2026-06-20 12:04:32 +01:00
dependabot[bot]
542f54f616
chore(deps): bump gitleaks/gitleaks-action from 2.3.9 to 3.0.0 (#2241)
Bumps [gitleaks/gitleaks-action](https://github.com/gitleaks/gitleaks-action) from 2.3.9 to 3.0.0.
- [Release notes](https://github.com/gitleaks/gitleaks-action/releases)
- [Commits](ff98106e4c...e0c47f4f8b)

---
updated-dependencies:
- dependency-name: gitleaks/gitleaks-action
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 05:57:39 +01:00
dependabot[bot]
21315f02c9
chore(deps): bump github/codeql-action from 4.36.0 to 4.36.2 (#2242)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.0 to 4.36.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](7211b7c807...8aad20d150)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 05:57:15 +01:00
Parafee41
e33f908775
ci: keep tree-sitter readiness summary counts current (#2196) 2026-06-18 05:14:44 +01:00
Gergő Magyar
3c82361b66
perf(cfg): streaming/chunked PDG graph emit for full-kernel-scale repos (#2202) (#2216) 2026-06-16 05:04:10 +01:00
Gergő Magyar
df08ecc397
perf(lbug): cut graph-DB emit/persistence wall time (#2203) (#2215)
* perf(lbug): add PROF_LBUG_LOAD persistence-path timing breakdown (#2203 U1)

loadGraphToLbug is un-timed today; the analyze 'emit' number is the
scope-resolution emit bucket, not the CSV->COPY persistence path. Add a
zero-cost-when-off per-stage breakdown (csv-emit/copy-nodes/rel-split/
copy-rels/fallback/total + node/rel counts) gated by PROF_LBUG_LOAD=1,
mirroring the PROF_SCOPE_RESOLUTION pattern. Document the flag in README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(lbug): route relationships to per-pair CSVs in the emit pass (#2203 U2)

Relationships were written once to a monolithic relations.csv, then re-read
line-by-line (regex per edge) and re-split into per-FROM->TO-label-pair files
before COPY — writing and reading the entire ~1M-edge set twice. Route each
edge to its pair file directly during the single emit pass via a shared
RelPairRouter, eliminating the monolithic write + re-read + per-edge regex.

The router applies the SAME getNodeLabel + validTables filter as the legacy
splitRelCsvByLabelPair, which is retained as a differential oracle. A new
differential test asserts the direct-emit per-pair files are byte-for-byte
identical to the oracle's, with identical skip/total accounting. The prof
line (U1) drops its rel-split stage (routing now folds into csv-emit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(lbug): skip per-row microtask tick in BufferedCSVWriter (#2203 U3)

addRow awaited an already-resolved promise on every buffered row, scheduling
a microtask per node even when nothing flushed (millions at scale). It now
returns a promise ONLY when it flushes; the node-emit loop awaits once per
iteration after the switch. Flush/drain semantics are unchanged, so
backpressure on the rows that actually write is preserved and the emitted
CSV bytes are byte-identical (covered by the determinism + differential tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): emit throughput + byte-identity gate for the persistence path (#2203 U4)

Build-free bench (bench/emit-persistence/measure.mjs) times streamAllCSVsToDisk
on a synthetic graph at two scales and gates: (1) an order-independent sha256
fingerprint over every emitted CSV line — the byte-identity guard for the U2/U3
emit optimisations — and (2) a scaling-ratio budget catching an O(n^2) emit
re-regression. Wired into ci-tests.yml alongside the cfg/scope-capture benches.
The LadybugDB COPY half needs a real DB, so its timing stays in PROF_LBUG_LOAD
+ the integration round-trip tests (documented in the bench README, with the
deferred COPY-parallelism follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply autofix feedback (#2203)

- P1: router backpressure drain-await rejected with a generic AbortError,
  masking the real EMFILE/disk-full error. Expose RelPairRouter.lastError and
  rethrow it in the emit catch — mirrors the oracle's throw streamError ?? err.
- P1: cover RelPairRouter error + backpressure + teardown paths with a new
  unit test (test/unit/rel-pair-routing.test.ts) using an injected mock stream.
- P2: wrap streamAllCSVsToDisk body in try/finally so the setMaxListeners bump
  is always restored (the U2 rel-routing throw path could leak it).
- P2: dedup WriteStreamFactory — re-export the canonical type from
  rel-pair-routing instead of a second identical declaration.
- P2: annotate splitRelCsvByLabelPair @internal as the retained differential
  oracle so a future dead-code sweep doesn't delete the byte-identity guard.
- P3: differential test now covers the proc_ prefix + clears
  GITNEXUS_SORT_GRAPH_OUTPUT to prevent env-leak desync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(lbug): scope byte-identity to quote-free ids + lock the quote-in-id divergence (#2215 review)

The 'byte-identical' claim was unconditional, but the router derives labels from
the raw id while the retained splitRelCsvByLabelPair oracle re-derives them via a
regex over the escaped row — so for an id containing a double-quote they diverge
(the router is the more-correct path). Soften the wording in rel-pair-routing.ts,
the bench README, and the differential-test comment to document the exception,
and add a differential test asserting the intended divergence (router routes the
quote-in-id edge; oracle drops it) so a future change can't silently revert to
the buggy regex semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): per-file fingerprint so the gate catches pair-file mis-routing (#2215 review)

fingerprintEmit flattened every line of every per-pair file into one array,
sorted globally, and hashed — losing file boundaries, so a row routed to the
WRONG pair file produced an identical fingerprint. Hash a per-file digest
(filename + sha256(file bytes)) and combine the sorted entry list, so mis-routing
(and within-file row reordering) now changes the fingerprint. Baseline
regenerated; the new scheme yields a different hash on byte-identical emit,
confirming it is sensitive to file structure the old flatten ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(lbug): add absolute large-scale wall-time backstop to the emit gate (#2215 review)

The scaling-ratio gate only compares large/small, so a uniform Nx slowdown at
both scales passes with ratio ~1.0. Add an opt-in max_ms_large ceiling (1000ms
vs observed ~200ms — generous, host-noise-tolerant) that --check enforces
alongside the ratio, catching a gross absolute regression the ratio misses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): cover the sorted-output path in the byte-identity differential (#2215 review)

The differential test only exercised the default insertion-order emit path. Add
a case under GITNEXUS_SORT_GRAPH_OUTPUT=1 that feeds the oracle the same
id-sorted order orderedRelationships() uses and asserts per-pair byte-identity,
so within-pair row reordering on the sorted path can't slip past the gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): cover the invalid-TO-label skip branch (#2215 review)

Only an invalid-FROM label was exercised; the validTables skip is an OR over
both endpoints, so the invalid-TO branch was untested (an inverted && would
have slipped through). Add a valid-FROM/invalid-TO edge to the differential
test and the router unit test, asserting it's skipped identically by router and
oracle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lbug): exercise the BufferedCSVWriter FLUSH_EVERY boundary in vitest (#2215 review)

The U3 addRow change (returns a flush promise only on flush; undefined when
buffered) and the loop's `if (pending) await pending` were only crossed by the
bench, never vitest (all fixtures are <500 nodes). Add a 600-node graph through
streamAllCSVsToDisk asserting all rows land exactly once across the 500-row
flush boundary — no drops, dups, or corruption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): drop redundant step cast in buildRelRow (#2215 review)

GraphRelationship.step is already typed number?, so (rel as { step?: number }).step
was a no-op structural cast that obscured the shared-type coupling. Use rel.step
directly. Byte-identical — bench fingerprint unchanged, differential test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): make the unknown-label node drop explicit (#2215 review)

With the U3 `let pending` switch idiom, a node whose label matches neither
codeWriterMap nor multiLangWriters left `pending` undefined and was silently
dropped — a footgun for a future node type. Add an explicit else with a comment
documenting that unknown labels are intentionally not persisted and that a new
type must be wired into a writer map. No behavior change (byte-identity + tests
unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(lbug): drop the unused WriteStreamFactory re-export (#2215 review)

The type was re-exported from lbug-adapter 'to preserve this module's surface,'
but no external code imports it by name from here (the only test reference is a
comment). Keep the import from rel-pair-routing.ts (its canonical home, still
used by splitRelCsvByLabelPair's signature) and drop the dead re-export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:40:59 +01:00
Gergő Magyar
6932e7a9fd
feat(cfg): PDG/CFG visitors for all supported languages (#2195) (#2197)
* test(cfg): validate cfg/visitors literals + drop 3 dead TS node types

Extend the grammar-literal CI gate (test/helpers/literal-collectors.ts)
to scan cfg/visitors/*.ts, mapping each visitor file to its grammar via
the existing basename rule (c-cpp -> C/C++, csharp -> C#, java -> Java,
go -> Go, typescript -> TS). Closes the gap where the gate never
validated CFG visitor node-type literals -- the prerequisite for adding
C-family visitors safely (#2195 U1).

The newly-scanned TS visitor surfaced 3 dead literals absent from every
grammar it serves (typescript/javascript/tsx all = 0): for_of_statement
(for-of parses as for_in_statement), async_function_declaration and
async_arrow_function (async functions are function_declaration /
arrow_function + an async child). Removed them; behavior-preserving --
the cases never matched, bench --check fingerprints unchanged, TS
visitor unit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): language-agnostic CFG unit-test harness (#2195 U1)

Extract the grammar-agnostic engine from ts-cfg-harness into
makeCfgHarness(grammar, visitor, filePath) at test/helpers/cfg-harness.ts.
Function discovery delegates to visitor.isFunction, so the harness carries
no language-specific node-type knowledge -- each C-family visitor's unit
tests can drive the real worker-side builder against real source.

ts-cfg-harness becomes a thin TS binding re-exporting the same
parse/collectFunctions/cfgOf/cfgsOf (behavior-preserving: all 5 existing
consumers -- taint propagate/model-match/summary-harvest/taint-emit + cfg
harvest -- pass unchanged, 223 tests green). New harness.test.ts proves
TS-faithfulness and isFunction-delegation via a stub visitor.

The bench parameterization (measure.mjs) is sequenced into U7, where the
first C-family scaling scenario makes the {grammar, visitorFactory} seam
validatable against a real non-TS language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): C and C++ CFG visitor + def/use harvest (#2195 U2)

Add createCCfgVisitor/createCppCfgVisitor over a shared CCfgWalk core.
Grammar introspection confirmed tree-sitter-c and tree-sitter-cpp share
every control-flow node type/field, so CppCfgWalk extends CCfgWalk with
only the C++-only nodes (try/catch/throw/for_range_loop/lambda) via a
visitExtra hook -- no language conditionals (AGENTS no-language-naming).
Wire both into c-cpp.ts providers.

Harvest (c-cpp-harvest.ts): two-phase binding table + per-statement
defs/uses/mayDefs (no sites[] yet -- U6). Edge kinds match the TS
contract; functionStartColumn populated; non-terminating loops (for(;;),
while(1)) emit the structural exit-escape edge so EXIT stays
reverse-reachable and CDG is not silently skipped -- verified against the
production post-dominator + control-dependence solvers (for(;;) -> 3 CDG
edges). buildFunctionCfg returns undefined rather than throwing.

23 real-parser regression tests; grammar-literal gate green (literals
validated against both grammars). Documented gaps: C++ RAII destructors,
setjmp/longjmp, computed goto (route to EXIT + warn).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): C# CFG visitor + def/use harvest (#2195 U3)

Add createCsharpCfgVisitor + csharp-harvest over the shared CfgBuilder /
ControlFlowContext, modeling the C# statement taxonomy: if/else,
for/foreach/while/do, switch_section (+ switch_expression arms),
try/catch/catch_filter/finally, using + lock (deterministic finalizers --
dispose/release runs on normal AND exception exit, finally-* completion
edges on crossing jumps), goto/labeled, yield (surface only), return/
throw/break/continue. Wire into csharpProvider.

Every literal validated against tree-sitter-c-sharp via the introspection
probe (record_declaration, no else_clause, switch_section, positional
access where no field exists). Edge kinds match the contract;
functionStartColumn populated; while(true) keeps EXIT reverse-reachable
(production CDG probe: 3 edges). buildFunctionCfg returns undefined
rather than throwing.

34 real-parser regression tests; grammar-literal gate green; no
regression (cfg unit dir 256/256, tsc clean). Documented gaps: yield
iterator state machine, goto case/default, async suspension points.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Java CFG visitor + def/use harvest (#2195 U4)

Add createJavaCfgVisitor + java-harvest over the shared CfgBuilder /
ControlFlowContext: if/else, classic for, enhanced-for, while, do-while,
classic-vs-arrow switch (switch_block_statement_group fallthrough vs
switch_rule no-fallthrough), try/catch/finally + try-with-resources
(auto-close synthesized as a finalizer, closes on normal AND exception
exit) + synchronized (monitor-release finalizer), labeled break/continue
to the labeled frame, yield, return/throw/break/continue. Wire into
javaProvider.

Every literal validated against tree-sitter-java via the probe
(switch_expression covers both switch forms, generic_type, line_comment,
for init field). Edge kinds match the contract; functionStartColumn
populated; while(true)/for(;;) keep EXIT reverse-reachable (production
CDG probe: 3 edges; hazard fixture: 34 CDG edges). buildFunctionCfg
returns undefined rather than throwing.

43 real-parser regression tests; grammar-literal gate green; no
regression (cfg unit suite 304, tsc clean). Documented gaps: switch-as-
expression-value inline, yield state machine, async/field-write defs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Go CFG visitor + def/use harvest (#2195 U5)

Add createGoCfgVisitor + go-harvest, the highest-divergence target:
for_statement (all four shapes -- for_clause C-style, while-style,
range_clause, bare for{}), expression/type switch (no implicit
fallthrough) + explicit fallthrough_statement, select_statement, defer
(LIFO finalizer legs at function exit), go (call is straight-line; the
closure body is its own CFG via isFunction), labeled break/continue/goto,
multiple-return assigns (a, b := f() defines each LHS). Wire into
goProvider.

CRITICAL (review A2): every non-terminating shape -- for{}, for cond{},
select{} with no default -- emits a structural exit-escape edge so EXIT
stays reverse-reachable and the production CDG is not silently skipped.
Verified: for{} -> CDG=3, select{} -> CDG=1, for-range -> CDG=2, all
exitReachable=true.

Every literal validated against tree-sitter-go via the probe. 32
real-parser regression tests; grammar-literal gate green; no regression
(186 across all 5 visitors + gate, full cfg unit 331, tsc clean).
Documented gaps: panic/recover unwind, goroutine happens-before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): call-site sites[] taint substrate for C-family (#2195 U6)

Extend the C/C++/C#/Java/Go harvests with the call-site sites[] taint
substrate (SiteRecord/SiteArgOccurrence), mirroring the TS shape so the
shared taint matcher consumes all languages uniformly. Extract the
grammar-agnostic site machinery into cfg/visitors/call-site-harvest.ts
(CallSiteFactAccumulator -- names no language); each harvest adds only its
per-grammar visitCall/walkChain over its call node (C/C++ call_expression,
C# invocation_expression, Java method_invocation, Go call_expression).

INERT BY DESIGN: no C-family taint model exists (registerBuiltinTaintModels
is TS/JS only), so getSourceSinkConfig returns undefined for these
languages and the harvested sites produce ZERO TAINTED edges -- the
positive source->sink->TAINTED path is deferred with the model authoring.

sites emitted only when non-empty; facts-only attachment, block/edge
topology unchanged (pre-existing topology + def/use tests byte-identical).
23 new substrate tests; 574 green across the cfg/taint/emit suites; gate
green; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): worker-mode PDG integration + bench parameterization (#2195 U7)

Prove the five C-family visitors build PDG through the REAL worker
pipeline. pipeline-pdg.test.ts: per-language (C/C++/C#/Java/Go) temp repo
run with pdg:true asserts BasicBlock+CFG+REACHING_DEF+CDG all > 0 (CDG>0
proves EXIT stays reverse-reachable end-to-end through the worker, incl.
each fixture's non-terminating loop/select); a paired run with pdg off
asserts == 0, the two flag-off graphs byte-identical (R3), no PDG types
leak, pinned by a golden snapshot. Counts e.g. Go 151 BB / 56 CDG.

Parameterize bench/cfg/measure.mjs by a per-language LANGS registry
resolved generically via getLanguageGrammar + getProvider(X).cfgVisitor
(no static import table). Default TS byte-identical -- all 6 TS
fingerprints unchanged under --check; taint-dense stays TS-only
(TS_JS_TAINT_MODEL never runs against model-less C-family CFGs). Add a
go:branchy scenario+baseline (namespaced) -- its fingerprint shape
(32 blocks/46 edges) matches TS branchy, cross-validating the Go visitor.

15 pipeline tests + bench --check PASS (7 scenarios); 354 unit cfg green;
dist rebuilt clean. Absorbs the bench parameterization deferred from U1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Python CFG visitor + def/use harvest (#2195 U8)

Add createPythonCfgVisitor + python-harvest -- the most structurally
divergent target (indentation blocks, elif, for/while-else, with, try/
except/except-group/else/finally, match/case, comprehensions, walrus),
confirming the shared CfgBuilder/ControlFlowContext core carries no
brace-family assumptions. for/while else-clause sits on the normal-
completion edge (not break); with modeled as try/finally dispose; match
has no fallthrough. Wire into pythonProvider.

Every literal validated against tree-sitter-python via the probe.
while True: keeps EXIT reverse-reachable (production CDG probe: 3 edges;
fixture: 42 CDG edges). 37 real-parser tests; gate green; no regression
(cfg unit 391, tsc clean). Gaps: async/generator suspension, comprehension
scope over-approximation. No sites[] (taint substrate, separate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): PHP CFG visitor + def/use harvest (#2195 U9)

Add createPhpCfgVisitor + php-harvest: if/elseif/else (+ alt colon
syntax), for/foreach/while/do-while, switch (fallthrough) + match (no
fallthrough), try/catch/finally, break N/continue N (N-th enclosing
loop), goto, return/throw. Wire into phpProvider.

Every literal validated against tree-sitter-php (php_only) via the probe
(for_statement initialize/condition/update; throw_expression not
throw_statement; break/continue integer child). while(true) keeps EXIT
reverse-reachable (production CDG probe: 3 edges; break 2 escapes the
outer loop). 35 real-parser tests.

Also repoint worker-roundtrip's "non-CFG language" gate test from Python
(which now has a cfgVisitor) to COBOL (the permanent non-goal of the
rollout) -- a stale assertion the Python commit invalidated. Full
in-process sweep green (452 across 18 files). Gaps: match inline value,
goto plain-block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Ruby CFG visitor + def/use harvest (#2195 U10)

Add createRubyCfgVisitor + ruby-harvest: if/unless/elsif/else +
statement-modifier forms (x if c, x while c), while/until/for (until
inverts the sense), case/when + case/in (pattern, no fallthrough),
begin/rescue/else/ensure (ensure=finally, rescue=catch) + retry
(loop-back into begin), return/break/next/redo, blocks/lambdas as their
own closure CFGs. Wire into rubyProvider.

Every literal validated against tree-sitter-ruby via the probe (case vs
case_match, modifier nodes, typed rescue/ensure children). loop do /
while true keep EXIT reverse-reachable (production CDG probe: 3 edges).
34 real-parser tests; comprehensive sweep green (486). Gaps: yield,
expression-position if/case/begin inline, ivar/gvar non-local defs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Rust CFG visitor + def/use harvest (#2195 U11)

Add createRustCfgVisitor + rust-harvest for the expression-oriented Rust:
if/else + if-let, loop (infinite -- structural escape edge), while/
while-let/for, match (no fallthrough) + guards, labeled break/continue
('outer), break-with-value, ? operator (try_expression) as an
early-return throw edge to EXIT, let-else (diverging else). visitLet
handles control-flow in value position (let x = loop/if/match). Wire into
rustProvider.

Every literal validated against tree-sitter-rust via the probe (label is
a named child not a field; line_comment; _ pattern). loop {} keeps EXIT
reverse-reachable (production CDG probe: 3 edges). 33 real-parser tests;
comprehensive sweep green (519). Gaps: panic, async/.await, macro bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Swift CFG visitor + def/use harvest (#2195 U12)

Add createSwiftCfgVisitor + swift-harvest (vendored tree-sitter-swift via
requireVendoredGrammar): if/else + optional binding (if let), guard...else
(diverging early exit), for-in/while/repeat-while (bottom-test), switch
(no implicit fallthrough; explicit fallthrough keyword; where guards),
do/catch + try/try?/try!, defer (LIFO finalizer at scope exit), labeled
break/continue, control_transfer_statement (one node for break/continue/
return/throw). Wire into swiftProvider.

Every literal validated against the vendored grammar via the probe (no
block node; if-let folds into condition+bound_identifier; defer parses as
a call_expression with trailing closure). while true keeps EXIT
reverse-reachable (production CDG probe: 3 edges). 24 real-parser tests;
comprehensive sweep green (543). Gaps: computed properties, defer
block-scope approx, fatalError traps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Kotlin CFG visitor + def/use harvest (#2195 U13)

Add createKotlinCfgVisitor + kotlin-harvest (vendored tree-sitter-kotlin):
if/else, when (subject + subjectless, no fallthrough), for/while/do-while,
try/catch/finally, jump_expression (return/return@/break/break@/continue/
continue@/throw), labeled loops, control_structure_body unwrapping,
expression-body functions. The grammar is field-less for control flow, so
the visitor navigates by child type+position. Wire into kotlinProvider.

Every literal validated against the vendored grammar via the probe
(line_comment/multiline_comment, not comment). while (true) keeps EXIT
reverse-reachable (production CDG probe: 3 edges; worker-mode fixture:
BB=82, CDG=41). 28 real-parser tests; comprehensive sweep green (571).
Gaps: value-position if/when/try inline, inline-fun non-local return,
getters/setters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Dart CFG visitor + def/use harvest (#2195 U14)

Add createDartCfgVisitor + dart-harvest (vendored tree-sitter-dart):
if/else, C-for/for-in/while/do-while, switch (empty-case fallthrough +
explicit continue-label) + switch_expression, try/on/catch/finally +
rethrow + assert (throw edges), return/break/continue/throw, labeled
loops, arrow bodies, closures. Dart splits a function into sibling
signature + function_body nodes, so the body (or function_expression) is
the CFG-bearing node. Wire into dartProvider.

Every literal validated against the vendored grammar via the probe (only
constant_pattern exists; removed speculative relational/logical pattern
names). while (true) keeps EXIT reverse-reachable (production CDG probe:
3 edges). 34 real-parser tests; comprehensive sweep green (605). Gaps:
labeled-loop grammar quirk (read via ERROR sibling), async straight-line,
value-position if/switch inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): Vue (reuse TS visitor) + worker-mode proof for all langs (#2195 U15)

Vue SFC <script> blocks are extracted and parsed with the TS grammar
(parse-worker languageMap[Vue] = TypeScript.typescript), so wire
vueProvider.cfgVisitor = createTypeScriptCfgVisitor() -- pure reuse, no
Vue-specific visitor. vue-visitor.test.ts replicates the worker path
(extractVueScript -> TS parse -> CFG) and confirms branch edges + EXIT
reverse-reachable + CDG>0.

Extend pipeline-pdg.test.ts with a worker-mode block covering all eight
remaining languages (Python/PHP/Ruby/Rust/Swift/Kotlin/Dart/Vue): per-
language temp repo, real worker pool, BasicBlock+CFG+REACHING_DEF+CDG all
> 0 with --pdg (CDG>0 proves EXIT reverse-reachable end-to-end through the
worker despite each fixture's non-terminating loop), == 0 without. Counts
e.g. Ruby 122 BB/45 CDG, Vue 49 BB/11 CDG. 30 pipeline tests green.

COBOL: documented as the deliberate PDG non-goal (no grammar, exotic
PERFORM/GO-TO control flow) in cobol.ts + the worker-roundtrip gate.

This completes PDG language coverage: every supported language except
COBOL now builds CFG/REACHING_DEF/CDG under --pdg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): surface skippedUnsoundFunctions in per-language stats (#2195 U2)

emitFileCdg computes skippedUnsoundFunctions (functions whose CDG is
withheld because EXIT isn't reverse-reachable from all blocks) but run.ts
dropped it on the floor — only cdgEdges/cdgDropped were aggregated. Add
the aggregation + a stats-line segment so CDG coverage gaps are an
explicit signal, not silent. Establishes the baseline skip count that
makes the U1 synthetic-escape pass's effect (the drop to genuine
anomalies only) measurable.

Additive; no emit-logic change. The emit-side field is covered by
cfg-emit.test.ts (asserts skippedUnsoundFunctions===1 + the warn on a
disconnected-block CFG); the run.ts aggregation is a thin pass-through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): synthetic-escape pass restores CDG for exit-unreachable cycles (#2195 U1)

Unconditional goto-cycles (C/C++/C#/Go) wire a backward seq edge with no
structural exit-escape edge, so EXIT becomes non-reverse-reachable and
emitFileCdg silently skipped ALL control-dependence for the function.

New cfg/synthetic-escape.ts: a pure deterministic SCC routine (iterative
Tarjan, sorted adjacency) + augmentForPostDom(cfg). No-op when EXIT is
already reverse-reachable (terminating fns + visitor-escaped loops are
byte-identical — returns the same object). Otherwise it batch-bridges
every exit-less SCC by adding an ANALYSIS-ONLY escape edge from the SCC's
controlling block (highest out-degree branch; lowest-index tie-break) to
EXIT, on a shallow-cloned FunctionCfg — never mutating persisted
cfg.edges. emitFileCdg threads that augmented view through BOTH
isExitReachableFromAllBlocks AND computeControlDependence (the Ferrante
walk re-reads cfg.edges, so a tree-only augmentation would be wrong).

Precision (anti-masking): only a trapped region containing a control
point (>=2-successor block) is bridged — a branch-less trapped region
carries no recoverable control-dependence and is indistinguishable from a
genuine construction anomaly, so it stays on the skip path (the existing
disconnected-block skip test still skips, skippedUnsoundFunctions===1). A
residual non-cycle dangling block is never bridged.

repro `void handler(int a){ start: if(a>0){work();} goto start; }`:
before exitReachable=false/CDG=0 → after one synthetic 2->1 edge,
exitReachable=true, exact CDG = {2->2:T,2->2:F,2->3:T,2->4:T,2->4:F}
(pinned exactly, not CDG>0 — catches a wrong representative). AC2 property
test extended to the augmented graph; per-language goto-cycle regressions
(C/C++/C#/Go). 199 cfg tests green; bench --check fingerprints unchanged
(analysis-only, zero persisted drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): isolate the non-terminating-loop hazard in worker CDG asserts (#2195 U3)

The pipeline-pdg worker-mode blocks asserted a whole-fixture cdg>0
aggregate (satisfied by any branching fn) while the comment claimed it
proved the non-terminating-loop EXIT-reachability end-to-end. Add a per-
language `hazard` marker + isolate the assertion: locate the hazard
function's BasicBlocks by its anchor and assert >=1 CDG edge is sourced
within it (a marker mutation now fails the test — non-vacuous). C# keeps
the aggregate (its fixture has no infinite loop). Comments corrected.

Switch the 7 visitor unit tests (java/csharp/dart/kotlin/php/swift/c-cpp)
from the local exitReachableFromAll CFG-shape helper to the production
isExitReachableFromAllBlocks + computeControlDependence on the hazard
function, matching go/python/ruby/rust/vue. 241 unit + 30 pipeline green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): gate vendored-grammar worker assertions on isLanguageAvailable (#2195 U4)

The Swift/Kotlin/Dart worker-mode pipeline-pdg cases require a vendored
grammar prebuild that may be absent on a CI platform — they'd go red
there. Mark those three REMAINING_LANGS entries `vendored` and gate both
the --pdg-on and --pdg-off `it`s on isLanguageAvailable(SupportedLanguages
[lang]) → it.skip when the grammar can't load. Installed-grammar
languages stay unconditional. Grammars present here, so all 30 run green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cfg): remove dead useCount() from swift + rust harvests (#2195 U5)

useCount() was declared on the local FactAccumulator in swift-harvest.ts
and rust-harvest.ts but never called (a copy-paste artifact; ruby's copy
IS used in an emit guard, so it stays). Pure deletion — the swift/rust
visitor suites stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cfg): standardize harvester API table()->bindingTable() (#2195 U9)

The binding-table accessor was named table() in the C/C++/C#/Go harvests
but bindingTable() in the other 7. Rename the 4 (definitions + their
visitor call sites) to the majority name bindingTable(). Pure rename; the
4 visitor suites stay green and tsc confirms no call site was missed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cfg): consolidate scope-tree substrate into ScopeTreeHarvester (#2195 U6)

The Go/Java/C#/C-C++ def/use harvesters each carried a byte-identical copy
of the lexical scope-tree machinery (Scope record, two-phase resolution
cache, openScope/nearestScopeOf/resolve/def/use/conditional/bindingTable,
~270 lines total). Extract it into an abstract ScopeTreeHarvester base; the
four harvesters now extend it and supply only their genuine per-language
variation (the prescan switch, plus Go's _-blank-identifier overrides of
declare/def/use). Net -422 lines. Mechanical and byte-equivalent: cfg unit
suite 613 passed, bench --check fingerprints unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cfg): consolidate no-site def/use accumulator into DefUseAccumulator (#2195 U7)

The Kotlin/Python/Ruby/Rust/Dart/Swift harvesters each carried a
byte-identical copy of the no-site def/use accumulator (~270 lines total;
only Ruby's adds the live useCount() emit-guard helper). Extract it as an
exported DefUseAccumulator beside CallSiteFactAccumulator in
call-site-harvest.ts (the PR's own model for the with-site superset); the six
harvesters import it under their existing local FactAccumulator name. Pure
byte-equivalent move, no logic change: cfg unit suite 613 passed, tsc clean,
bench --check fingerprints unchanged (TS/Go paths untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): consolidate copied visitor-test helpers into cfg-harness (#2195 U8)

The 13 *-visitor.test.ts files each copied a byte-identical set of CFG-shape
helpers (edgeKinds/block/reaches/reachable/bindingIdx/allSites/hasAnySites,
~380 lines total). Export them once from test/helpers/cfg-harness.ts and import
per file (only the subset each references). Also drop each file's local
exitReachableFromAll — a re-implementation of the production
isExitReachableFromAllBlocks (semantically identical: false iff some
entry-reachable non-EXIT block can't reach EXIT) — and point its live call
sites at the already-imported production function. Pure test-only mechanical
move, behavior-preserving: tsc clean, test/unit/cfg/ 613 passed unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): pin *-harvest.ts literals to their own grammar in the gate (#2195 U10)

The grammar-literal validation gate scans cfg/visitors/, but a <lang>-harvest.ts
basename was not in BASENAME_LANGS, so fileLanguages() fell it through to the
weak ALL_LANGS valid-if-any bucket — a node-type literal dead in its own grammar
but valid in some other grammar would pass undetected. Strip the -harvest suffix
and reuse the visitor basename map so go-harvest -> Go, c-cpp-harvest -> C+C++,
typescript-harvest -> TS, etc. The two language-agnostic harvesters
(call-site-harvest, scope-tree-harvest) name no grammar and stay valid-if-any.
Also corrects the now-inaccurate mode2Files comment. Adds a fileLanguages unit
test; the existing gate stays green (no harvest file has a dead literal), and a
scratch probe confirmed a bogus go-harvest literal is now caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): defensive per-statement cap on harvested taint sites (#2195 U11)

A statement's harvested sites[] had no explicit bound — a pathological or
machine-generated statement (hundreds of nested calls) could grow it without
limit. Add DEFAULT_PDG_MAX_SITES_PER_STATEMENT (512, mirroring the PDG edge/fact
cap style): openCallSite/addMemberRead check-before-push and stop at the cap,
keeping the first 512 sites fully intact and setting an observable
sitesTruncated flag. A cap-dropped openCallSite returns a -1 sentinel that
pushFrame/setSite*/the occurrence fan-out all tolerate (no dangling parent/via,
no clobber of kept sites). Generous enough that no real statement is affected:
bench --check fingerprints unchanged, cfg unit suite 617 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(codeql): exclude nested test fixtures from the CodeQL gate (#2195)

The CodeQL results gate failed on test/integration/cfg/fixtures/python-hazards.py
('total' may be used before init, unused vars) — but that file is an intentional
CFG/PDG hazard fixture, exactly the synthetic broken-code the existing
'**/test/fixtures/**' exclusion is meant to skip. That glob does not match the
deeper test/integration/cfg/fixtures/ path, so the hazard fixtures leaked into
the scan. Add '**/test/**/fixtures/**' to cover fixtures nested anywhere under a
test tree. Analyze (python) and Analyze (javascript-typescript) both already pass
— production code is clean; this only silences fixture noise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(cfg): apply prettier + drop unused imports across the PDG files (#2195)

The merge of main into this branch pulled in the stricter quality gates
(prettier --check . and eslint .), which surfaced pre-existing formatting in the
PDG/CFG rollout (line-width wrapping across the visitor + harvest files, bench,
tests) plus 9 no-unused-imports errors. Mechanical autofix only — npm run
format + lint:fix equivalent, scoped to gitnexus/: removes unused FunctionCfg/
SiteRecord type imports left by the U8 helper consolidation and stale
FinalizerFrame imports in python.ts/ruby.ts. No behavior change: tsc clean, cfg
unit suite 617 passed, eslint 0 errors. (gitnexus-web class-order noise is a
local tailwind-plugin artifact CI does not flag — left untouched.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest C++ structured-binding defs (auto [a,b]=e) (#2195)

The C++ def/use harvester only recorded a def when an init_declarator's
declarator was a plain identifier, so a structured binding (auto [a,b] = mk(),
incl. the auto& reference form whose binding sits under a reference_declarator)
declared only the first name in phase 1 and emitted ZERO defs in phase 2 — a,b
were walked as spurious uses and later use(a)/use(b) resolved to a synthetic
module binding, silently corrupting REACHING_DEF/taint for an idiomatic C++17
shape. Unwrap the structured_binding_declarator in both phases and def every
identifier leaf; result-of-initializer flows to the whole list. Inert for C
(no structured bindings). Characterization tests added (plain + reference form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): model C++ co_return as a return terminator to EXIT (#2195)

co_return_statement was neither in CPP_CONTROL_FLOW_TYPES nor dispatched, so a
coroutine's co_return coalesced into a straight-line block and emitted a
spurious seq fallthrough to the following statement instead of an edge to EXIT
— statements after co_return looked reachable and the terminator edge was
missing, corrupting CFG/CDG for coroutines. Add the node type to the C++
control-flow set and dispatch it through visitReturn (block -> EXIT 'return',
no fallthrough). C path untouched; co_await/co_yield remain plain expressions.
Characterization test added; c-cpp suite + grammar-literal gate green, bench
--check unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest C# out-var and deconstruction-declaration defs (#2195)

Two idiomatic C# write shapes recorded ZERO defs, silently breaking
REACHING_DEF/taint:

- out-var (G(out var n) / G(out int n)) parses as a declaration_expression;
  it was neither declared (phase 1) nor def'd (phase 2), so n resolved to a
  synthetic module binding and the callee-written value had no reaching def.
- deconstruction declaration (var (a, b) = T()) has a variable_declarator whose
  name slot is a tuple_pattern (null name field), so declareVariableDeclaration
  + the variable_declaration walk skipped it entirely (only the assignment form
  (a,b)=T() was handled). Both a and b were dropped.

Declare + def the declaration_expression's identifier (must-def: out params are
definitely-assigned), and route a null-name variable_declarator through the
tuple_pattern via the existing declareForeachTarget/defTupleTargets helpers.
Characterization tests added; csharp suite 42 passed, grammar gate + bench
--check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): put embedded-script CFGs in file coordinates via lineOffset (#2195)

A Vue SFC <script> block parses at row 0 but lives at lineOffset in the .vue
file. Every other worker-emitted graph node adds lineOffset to reach file
coordinates, but collectFunctionCfgs built FunctionCfgs from the extracted
script's raw rows and never offset them. Two consequences for .vue files:
- inter-procedural taint silently resolved NOTHING — the summary-harvest join
  keys graph Function/Method nodes by their (offset) startLine but looked up the
  CFG's (unoffset) functionStartLine, missing by exactly lineOffset, so no
  FunctionSummary was ever produced;
- persisted BasicBlock startLine/endLine (and the id's functionStartLine
  segment) pointed at the wrong .vue line, breaking source mapping.

Thread lineOffset into collectFunctionCfgs and shift every CFG source-line field
(functionStartLine/End, block start/end, statement + non-synthetic binding
lines) into file coordinates at the one production chokepoint. A 0 offset
returns the CFG unchanged, so .ts/.js/etc. stay byte-identical (bench --check
fingerprints unchanged; worker-roundtrip + pipeline-pdg green). Unit tests for
the shift + the 0-offset no-op added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): surface CDG soundness skips at warn, not just debug (#2195)

skippedUnsoundFunctions (a function whose EXIT is not reverse-reachable from
all blocks, so control dependence is withheld) was only reported inside the
per-language logger.debug stats line — while the taint/RD coverage-gap and
cap-drop counts surface unconditionally at warn. A language that systematically
trapped EXIT (an unmodeled non-terminating / multi-terminal shape the
synthetic-escape pass can't bridge) would silently lose all CDG. Add a parallel
unconditional warn (R8) alongside the R4 taint-gap warn. Observability only —
no graph change; emit-layer skip counting stays covered by cfg-emit's
skippedUnsoundFunctions test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest Kotlin x++/--x as a def (#2195)

The Kotlin harvester had no postfix_expression/prefix_expression case, so an
increment/decrement fell to the default descent and recorded its operand as a
use only — never a def. Every sibling harvester (Java/C#/C++/Dart/TS/PHP) models
inc/dec, so a Kotlin counting loop (while/for using i++) silently dropped the
loop-carried reaching-def of the counter. Add the case: def AND use the operand
when it is a plain simple_identifier and the operator is ++/-- (other pre/postfix
forms — -x, !x, x!!, x? — stay pure reads, byte-identical to the old descent).
Characterization tests for postfix + prefix added; kotlin suite 30 passed,
grammar gate + bench --check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest Go select channel-receive binding as a def (#2195)

walkValue had no receive_statement case, so a select receive (case v := <-ch:)
fell to the default descent: v was recorded as a USE of an uninitialized var
and the channel-sourced definition was invisible to REACHING_DEF/taint —
channels are a primary taint source in Go. Add the case mirroring
short_var_declaration: def each left identifier, use the <-ch right, attach
resultDefs for the := short form. prescan already declared the binding; this
completes the phase-2 fact. go:branchy bench fingerprint unchanged; go suite
40 passed, grammar gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest all names of a Dart multi-variable declaration (#2195)

`var a = 1, b = 2;` is one initialized_variable_definition whose first binding
is the name/value field pair and whose subsequent bindings are trailing
initialized_identifier children. Both prescan (declareInitializedVar) and the
walkValue case read only the name/value fields, so every name after the first
was never declared or def'd — `b` resolved to a synthetic module binding and
its REACHING_DEF/taint flow was lost. Iterate the trailing initialized_identifier
nodes in both phases. (Dart-3 record/list pattern declarations `var (a,b)=pair`
remain a separate follow-up.) dart suite 35 passed, tsc + grammar gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): bind Swift switch-case value patterns (case let n) (#2195)

A switch value-binding (case let n where …, case .some(let v)) was never
declared in prescan, so n/v resolved to a synthetic module binding and a body
use(n) did not link to any def — a very common Swift idiom silently lost its
data dependence. Declare the switch_pattern's bindings (prescan, reusing
declarePattern) and emit them as MAY-defs on the dispatch block (a case may not
match) via a new switchPatternFacts, propagated into the case body. swift suite
25 passed, tsc + grammar gate green. (The rare ?? / ternary-arm may-def — Swift
assignment-as-expression — remains a separate follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): wire Swift multi-catch throw edges to every handler (#2195)

visitDo routed the protected body's throw edge only to handlerEntries[0], so a
do { try r() } catch A {} catch {} left the 2nd..Nth catch handlers UNREACHABLE
from ENTRY — orphaned blocks whose error bindings + def/use facts were stranded
in a dead component (a soundness gap for idiomatic Swift typed multi-catch).
Swift tries the catch clauses in order and the thrown type is unknown at CFG
time, so every protected block may reach ANY clause: edge each protected block
to every handlerEntry. Found by the per-language CFG/CDG verification swarm
(reproduced: 2-catch=1, 3-catch=3 unreachable blocks). swift suite 26 passed,
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): synthesize a protected block for an empty Kotlin try {} (#2195)

An empty `try {}` body produced zero protected blocks, so visitTry's throw-edge
loop wired nothing to the catch and the try's entry fell through to the finally
— leaving the catch handler block + its error binding orphaned (unreachable from
ENTRY), a malformed CFG with stranded def/use facts. Mirror the existing
empty-`catch` synthesis: when the try body is empty and there is a catch or
finally, synthesize one protected block so the catch handler(s) are wired and
the try entry is the body, not the finally. Found by the per-language CFG
verification swarm. Non-empty try is byte-identical; kotlin suite 31 passed,
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(cfg): bound the reaching-defs fixpoint with a per-block visit ceiling (#2195)

The per-language verification swarm reproduced, AT PRODUCTION DEFAULTS, a
reaching-defs blow-up: a machine-generated ~2000-line all-loops function (under
DEFAULT_PDG_MAX_FUNCTION_LINES) reaches ~10k basic blocks because loops emit ~5
blocks/line, and the dataflow fixpoint is O(blocks^2.3) on deep loop nests —
measured 62s (C/C++) and 2.05s + 810MB (Go) for ONE function. maxFacts does not
help: the fact count stays LINEAR, so it never fires.

Iterative reaching-defs on a reducible CFG converges in O(loop-nesting-depth)
passes, so a worklist re-visits each block a small multiple of times for real
code. Add a maxBlockVisits ceiling (emit passes blocks.length × 64 — far beyond
any hand-written nesting depth, ~15) that bails when the fixpoint has not
converged. An unconverged fixpoint's in/out sets are not sound, so it returns
NO facts (status 'truncated', like the existing 'overflow' guard) — a per-
function coverage gap, never wrong facts. Real code is byte-identical: full cfg
suites 725 passed, bench --check fingerprints unchanged.

NOTE: computeControlDependence's O(N²) up-walk on deep post-dom chains is the
sibling concern but stays ~13ms in production (bounded by the line cap + the
CDG materialization cap); a CDG work-budget is a documented follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): raise the parse-worker stack limit for deep CFG recursion (#2195)

The CFG visitors build per-function control-flow graphs by recursive descent
over the tree-sitter AST, so deeply-nested source overflows the worker thread's
call stack (~1.5k nesting levels) — caught per-function (R4 try/catch) but the
function silently gets no PDG. A worker thread's stack is governed by
resourceLimits.stackSizeMb (Node default 4 MB); the main process's
--stack-size=4096 flag does NOT propagate to worker threads (confirmed by prior-
art research on Node worker_threads). Raise it to 16 MB, pushing the overflow
threshold to several-thousand nesting levels — far beyond any hand-written code,
so only machine-generated/obfuscated nesting can still hit it (and that stays a
caught per-function skip, never a crash). Complements a future proactive depth
guard. pipeline-pdg worker tests 30 passed, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(cfg): compute control dependence as a reverse-CFG dominance frontier (#2195)

The Ferrante §3.1.1 up-walk re-climbed the ipdom chain once per CFG edge,
which is Θ(N²) on a deep post-dom chain (a single branch fanning into a
shared spine took ~7.1s at 16k blocks). Replace it with the reverse-CFG
post-dominance-frontier formulation (Cytron, Ferrante, Rosen, Wegman &
Zadeck 1991): control dependence IS the dominance frontier of the reverse
CFG, computed bottom-up over the post-dom tree (PDF_local from a node's CFG
in-edges + PDF_up from its post-dom-tree children) in O(N + E + output).
LLVM (ReverseIDFCalculator), Joern (CdgPass) and WALA use the same form.

Output is the IDENTICAL deduped/sorted (controller, dependent, label) set:
verified byte-identical across all cfg unit+integration suites, the
cdg-snapshot oracle, and bench --check fingerprints (unchanged). The PDF
unions a label SET per (controller, dependent) pair, preserving the
multi-label rows the old per-row dedup kept on opposite-sense (goto-cycle)
arms. buildArmSenses, labelFor, the final sort and the maxEdges truncation
cap are kept verbatim; the post-order walk is iterative so a chain-deep
post-dom forest cannot overflow the stack.

Adds three regressions: multi-label-per-pair preservation, the literal
self-edge / NO_IPDOM seed guard (a !== x), and a fan-into-chain perf
tripwire (linear vs the former quadratic up-walk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cfg): record the reaching-defs WTO no-go decision (#2195)

Weak-topological-order / loop-aware iteration (Bourdoncle 1993) was
evaluated as the fix for the O(blocks²) deep-loop-nest blow-up and
rejected: a faithful WTO solver was 104/104 byte-identical to the RPO
worklist but 0% faster — the cost is inherent dense-set propagation +
lattice merges, not visitation order, and the loop-body-skip shortcut is
unsound on irreducible (goto) CFGs. Document this at the RPO-order site
and the emit.ts revisit-ceiling constant so the shipped blocks×64 bound
reads as the sound backstop it is, with SSA-sparse reaching-defs named as
the deferred real fix. Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cfg): proactive visitor nesting-depth guard + observable CFG skips (#2195)

The CFG visitors are recursive-descent with no shared base, so a
pathologically nested function (machine-generated / adversarial) could
overflow the worker's native stack — a nondeterministic RangeError that
escaped to the language-group catch and silently dropped EVERY remaining
file's CFG.

Guard it proactively: CfgBuilder tracks live recursive-descent nesting
depth via enterNesting/exitNesting, called at each visitor's visitBody and
visitSeq choke points (visitBody covers nested control constructs incl.
else-if ladders; visitSeq covers deeply-nested bare blocks). Exceeding
MAX_CFG_NESTING_DEPTH (500, far below the ~1.2k+ native limit and far above
real code's ≤~50) throws a typed, DETERMINISTIC CfgNestingDepthError instead
of waiting for the engine's nondeterministic overflow.

collectFunctionCfgs now isolates the build PER FUNCTION: the depth bail or
any other throw is caught, counted, and skipped — one bad function no longer
loses the whole file's CFGs. CollectedCfgs.skipped widens from a bare number
to reason-counted buckets (tooManyLines / tooDeeplyNested / buildError). The
worker stops discarding that count (parse-worker.ts), aggregates it
per-language onto ParseWorkerResult.cfgSkipped (survives the parse cache via
slim's `...result`), and mergeChunkResults merges + warns per-language so a
CFG coverage gap is observable, not silent.

Behavior-preserving on normal code: the guard never fires below 500 nesting,
so the cfg unit+integration suites (731), the CDG/RD/CFG snapshots and the
bench --check fingerprints are all byte-identical. The worker stackSizeMb
4→16MB bump shipped earlier (de4c43a4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cfg): unify the nesting guard behind CfgBuilder.withNesting (#2195)

Tri-review flagged the visitBody/visitSeq guard asymmetry (visitBody used
try/finally; visitSeq placed a bare exitNesting before its tail return). It
is not a bug today — the CfgBuilder is per-function and discarded on a bail,
so a leaked counter is never read — but a future mid-loop return in visitSeq
would silently corrupt the depth count. Replace both hand-paired sites in all
12 visitors with a single `CfgBuilder.withNesting(fn)` helper that enters on
the way in and exits in a finally, so the pair can never drift. Also document
that block-bodied constructs pass through BOTH choke points, so the effective
lexical ceiling is ~MAX_CFG_NESTING_DEPTH/2 (~250).

Behavior-preserving: 733 cfg tests + bench --check fingerprints byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): include cfgSkipped in all parse-worker result initializers (#2195)

Tri-review found cfgSkipped omitted from the two reset/fallback
ParseWorkerResult initializers (only the main one carried it). The field is
optional with `?? {}` reads so there is no runtime bug, but the zero-state
initializers should be complete and consistent. Also correct the field's
doc-comment: the per-language merge + warn lives in `dispatchChunkParse`
(alongside skippedLanguages), not `mergeChunkResults` — and, like that
sibling telemetry, the warn fires for freshly-parsed chunks, not on a warm
cache hit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cfg): scope the CDG byte-identical claim to the untruncated output (#2195)

Tri-review noted the rewrite's `maxEdges` truncation now trims the SORTED
edge set, whereas the old up-walk broke mid-walk in CFG-edge-iteration order
— so a truncated PREFIX can differ at the cap boundary (only when a single
function exceeds maxEdges; the FULL untruncated set is byte-identical, now
also confirmed by ~1M-case differential fuzz). Clarify the module doc and the
maxEdges param doc: the cap bounds OUTPUT count (peak working set ≈ output in
the DF formulation, not the old pre-dedup spike), and the byte-identical
guarantee is scoped to the untruncated output. Comments only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cfg): strengthen depth-guard, perf-tripwire and buildError coverage (#2195)

Tri-review test-quality findings:
- cfg-builder: capture the thrown CfgNestingDepthError unconditionally (a
  catch-only assertion silently passes if a future change stops throwing);
  add a withNesting test that the counter balances on the THROW path too.
- control-dependence perf tripwire: assert controller IDENTITY (every edge
  controlled by block 0, distinct dependents in range), not just length, so a
  fast-but-wrong reimplementation can't pass on the M-1 count alone.
- worker-roundtrip: add the missing buildError test — a generic (non-depth)
  buildFunctionCfg throw is caught per function, counted under buildError, and
  does NOT drop the file's sibling CFGs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): model Kotlin value-position when/if as control dependence (#2205)

Idiomatic Kotlin uses `if`/`when` as EXPRESSIONS (`val x = when (k) { … }`,
`return if (c) a else b`, `fun f() = when (k) { … }`), but the visitor only
modeled them as control flow in statement position (the `isStatementPosition`
gate) — value-position branches collapsed into one straight-line block, so
their arms emitted no control dependence. Cross-language PDG validation
(#2195, PR #2197) measured the result: two Kotlin repos at 3% / 6% CDG per
BasicBlock vs 18–76% for every other language (incl. the other
expression-conditional languages, Rust 30% / Swift 24%).

Model value-position `when` (≥2 arms) and `if`/`else` as control flow in the
three dominant carriers — `property_declaration` (rejoin the arms at a
binding continuation carrying the bound name's def), `return`, and the
`fun f() = …` expression body (each arm returns) — mirroring the Rust
visitor's value-position `let` handling. `visitWhen`/`visitIf` are reused
unchanged; `isControlFlow` now routes a value-branch `val`/`var` decl to the
branch handler instead of coalescing it.

Measured: Exposed CDG 3636→5644 (+55%, 6%→9% of BasicBlocks), turbine
67→82 (+22%). Argument-position branches, assignment RHS, and value-position
`try` are left inline — a remaining gap tracked on #2205.

Behavior-preserving for non-Kotlin (bench --check byte-identical; 739 cfg
tests incl. 6 new value-position regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): model Ruby value-position if/case on assignment RHS as control dependence (#2195)

`if`/`case` are expressions in Ruby; as an `assignment` RHS
(`x = if c then a else b end`, `x = case k … end`) the visitor previously
left the arms INLINE in one coalesced block (a gap documented at ruby.ts
§"if/case/begin are EXPRESSIONS"), emitting no control dependence. Model the
RHS branch as control flow and bind the LHS at the rejoin: `assignmentBranch`
detects the carrier, `visitSeq` routes it out of the coalescing path, and
`visitBindBranch` reuses `visitIf`/`visitCase` + a facts-only continuation
carrying the LHS def (new `harvest.assignmentDefFacts`). Mirrors the Kotlin
(#2205) and Rust value-position handling.

Honest impact: SMALL in practice — rack CDG 1269→1303, sinatra 1212→1232
(~+2–3%). `x = if/case` is far rarer in idiomatic Ruby than the Kotlin
analog (Ruby favors ternary / `||=` / guard modifiers), and Ruby's low
CDG/BB is mostly structural (micro-branches `&&`/`||`/`?:`/`&.` excluded by
design, plus many straight-line `.each`/`.map` block CFGs). This closes the
documented gap correctly; it is not a large ratio mover. Explicit
`return if … end` is NOT a carrier — tree-sitter-ruby drops that value; the
idiomatic implicit-last-expression conditional was already modeled.

Behavior-preserving for non-Ruby (bench --check byte-identical; 744 cfg
tests incl. 5 new value-position regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): keep an empty-arm Kotlin when wired to the join (#2195)

An all-empty-arm `when` with an `else` — `when(k){0->{};else->{}}`, idiomatic
`else -> {}` "do nothing" — left the dispatch block with ZERO successors:
empty arms got no `switch-case` edge, and the `else` suppressed the no-match
edge. The dispatch and its join then became orphaned, so
isExitReachableFromAllBlocks returned false and emitFileCdg silently dropped
the ENTIRE function's control dependence (counted cdgSkippedUnsound). The
#2205 value-position fix newly routes `val x = when(…)` / `return when(…)` /
`fun f() = when(…)` through visitWhen, exposing it in those carriers too.

Wire every arm (empty or not) to the join, so the dispatch always has a
successor. Found by the per-language CFG verification swarm. Behavior-
preserving elsewhere (bench --check byte-identical; cfg suite green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): wire C/C++ throw edges to every catch handler, not just the first (#2195)

`visitTry` edged every protected-region block only to `handlerEntries[0]`, so
in a multi-`catch` (`catch(int e){…} catch(double d){…} catch(...){…}`) the
2nd..Nth handlers were orphaned — unreachable from ENTRY, their catch-param
binding and body control/data flow silently lost. The runtime catch that
matches a thrown type is not statically known, so over-approximate: edge each
protected block to EVERY handler entry (mirrors the Swift multi-catch
handling). Found by the per-language CFG verification swarm; the two existing
exception tests both used a single catch, so it was never exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): keep a bare continue in a Dart switch case — it targets the loop (#2195)

`caseStatements` stripped EVERY `continue_statement` from a case body, but only
a LABELED `continue LABEL;` is a switch fallthrough-spill (handled via
caseContinueLabel). A bare `continue;` targets the ENCLOSING LOOP (valid Dart);
dropping it removed the jump and fabricated a false case → next-statement
fall-through edge (e.g. `case 1: tainted(); continue; default: sink();` made
tainted() flow directly into sink()). Only strip the labeled form; a bare
`continue;` stays in the body and routes to the loop. Found by the per-language
CFG verification swarm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest Rust match-arm pattern bindings as defs (taint propagation) (#2206)

`visitMatch` visited arm bodies but never harvested the arm PATTERN's bindings,
so `match x { Some(n) => sink(n) }` left `n` with a use and no def/may-def —
taint from the matched subject could not propagate into the arm. Add
`matchArmPatternFacts` (the binders as MAY-defs, since only the matching arm
binds) and attach it to the dispatch block, co-located with the subject's use.
Found by the per-language CFG verification swarm; the match tests asserted
`hasUse` but never `hasDef`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): harvest Swift guard case / if case enum-pattern bindings as locals (#2206)

`guard case .some(let v) = e` / `if case let .x(n) = e` nest the binder inside a
`pattern` condition child, not a direct `bound_identifier`. Both the declaration
(declareOptionalBindings) and the def-facts (conditionFacts, which ran walkValue
= a USE on the pattern) missed it, so the binding resolved to a synthetic
`@module` global with a use and no def — breaking taint propagation from the
subject. Declare the `pattern` child and def its leaves (may-def when
conditional). Found by the per-language CFG verification swarm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): treat Dart switch-expression arm writes as may-defs, not hard kills (#2206)

`DartHarvester.walkValue` had no `switch_expression` case, so an assignment in an
arm value — `var y = switch(x){ 1 => z = 10, _ => z = 20 }` — became an
unconditional def that KILLED the prior `z`, even though only one arm runs (the
module docstring claimed it was a may-def, but the code didn't implement it).
Walk the subject always and each `switch_expression_case` under `conditional(…)`,
so arm writes are may-defs — mirroring `conditional_expression`. Found by the
per-language CFG verification swarm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): model the C# `using var` declaration-form dispose finalizer (#2206)

`using var f = Open();` (C# 8) parses as a `local_declaration_statement` with a
leading `using` keyword, not a `using_statement`, so visitUsing never ran — no
dispose block, and a `return`/`break`/`continue` in its scope got no
`finally-*` completion edge. Unlike the delimited block form, its dispose runs
at ENCLOSING-SCOPE exit, so visitSeq now treats the REST of the sequence as the
protected body: the acquisition (`var f = e`) is a normal block outside the
dispose region (a throw there means the resource was never acquired), and
`buildUsingDeclScope` wraps the remainder in a synthetic dispose finalizer
(normal + exception exit, early exits thread through) — mirroring
buildProtectedSynthetic. Closes the last #2206 item. Found by the per-language
CFG verification swarm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:31:04 +01:00
Gergő Magyar
96dc368d96
fix(ci): align tree-sitter readiness + grammar-update workflows on a shared manifest (#858) (#2187)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* chore(ci): add shared vendored-grammars manifest; monitor reads it

.github/vendored-grammars.json is the single source of truth for the vendored
tree-sitter grammars (c/swift/kotlin/dart/proto): name, upstream coords, and
policy holds. update-vendored-grammars.mjs now builds its GRAMMARS map from the
manifest (behavior-preserving — same exported shape). Adds manifest-agreement
tests so the loader can't silently skew from the file.

* fix(ci): classify vendored grammars from manifest, drop bare "?" (#858)

The readiness report decided "is this vendored?" via is_vendored_pin (a file:
package.json spec) — but the 5 vendored grammars aren't in package.json, so they
were misrouted through the npm path and rendered bare "?" for ABI (read from an
empty node_modules), plus a spurious "? (fetch failed)" for github-only proto.

Now vendored grammars are classified by membership in the shared manifest and
their ABI is read from gitnexus/vendor/<name>/src/parser.c (always in a
checkout). github-only vendored grammars skip the npm peer-dep fetch; the
tree-sitter-c hold is surfaced from the manifest (held, not plain "Ready"); and
every remaining unintrospectable value renders a labeled token, never a bare
"?". --assert-current now covers the vendored grammars too instead of skipping
them. Adds a stdlib unittest suite incl. a manifest⇄vendor-dir consistency
guard.

* docs(ci): document the shared vendored-grammars manifest

Both tree-sitter workflow headers now point at .github/vendored-grammars.json as
the shared source of truth; the readiness workflow gains a PR-path trigger on the
manifest + test, and runs the readiness unit tests on validation events.
CONTRIBUTING.md documents the manifest contract under CI automation contracts.

* fix(review): apply autofix feedback

- Guard manifest reads in both scripts with a clear error (was an opaque
  module-import traceback that crashed the script and test collection).
- Never render a bare "?": relabel the npm-path ABI/version/peer sentinels and
  the vendored upstream-ABI miss to labeled tokens; the report is now ?-free
  regardless of node_modules/network, and the test is hermetic.
- Add a VENDORED_NAMES ⊆ GRAMMARS guard + manifest-missing error test.
- Drop now-dead is_vendored_pin/is_vendored/_(vendored)_.
- Compose held + out-of-range vendored blocker reasons instead of overwriting.
- Reword the shared-manifest docs to not over-claim shared upstream coords.

* fix(ci): apply root prettier formatting to mjs + ts test

The quality/format gate runs root `prettier --check .` (printWidth 100, the
gitnexus-local config differs and falsely passed locally).

* test(ci): make both tree-sitter scripts testable offline

The scripts hit live npm/GitHub, which makes the report run flaky and the
monitor's detect/apply logic untestable. Add hermetic seams:

- readiness: --offline flag (+ GITNEXUS_TS_READINESS_OFFLINE env) no-ops the npm
  registry + upstream fetches; the report renders deterministically (vendored
  ABIs from the repo, npm columns marked 'offline', no bare '?'). 3 tests assert
  an offline run touches ZERO network (urlopen patched to raise).
- monitor: detect() and apply() accept injected deps (vendoredVersion/
  resolveUpstream/fetchSource/readAbi) so the newer/ABI/hold gating runs offline
  with fixtures; apply gains --dry-run (validates but writes nothing). 6 tests
  cover newer/same-version/held-c/ABI-15/applicable + a no-mutation dry-run.

* fix(review): keep --assert-current hermetic + harden the no-bare-? invariant

Tri-review findings (PR #2187):
- P2 REGRESSION: --assert-current (documented 'hermetic and offline', run in CI
  without --offline) routed the 5 vendored grammars through vendored_drift_summary,
  which fetches upstream parser.c + commit sha — 10 discarded network calls per run.
  Fix: read the vendored ABI locally via a new vendored_abi_from_repo() helper (also
  used by vendored_drift_summary). Now verifiably network-free.
- Unify the upstream-ABI miss sentinel: prose said 'n/a (generated at build)' while
  the matrix said 'n/a' — and 'generated at build' is a wrong cause (swift HAS a
  committed parser.c). Both now render neutral 'n/a'.
- Fix the stale assert_current docstring claiming swift is prebuilt-only/no parser.c.
- Guard the last latent bare-? path (vendor package.json missing 'version').

Tests: AssertCurrent (network-free guard + out-of-range via the new injection
point), malformed-JSON manifest, detect() error-path, explicit npm/github
undefined assertions. 17 Python + 15 vitest, all hermetic.

* fix(review): use a single unittest import style (CodeQL 753)

CodeQL py/import-and-import-from flagged `import unittest` + `from unittest
import mock`. Collapse to `from unittest import TestCase, main, mock`.

* fix(review): explicit raise in _matrix_row (CodeQL 754)

CodeQL py/mixed-returns flagged the implicit fall-through after self.fail()
(which it doesn't model as NoReturn). End with an explicit raise AssertionError.

* test(review): replace non-null assertions with a must() guard

@typescript-eslint/no-non-null-assertion flagged 4 `!` operators. Add a
narrowing must<T>(value, message) helper (throws on undefined) and a named
baseResolveUpstream, removing every non-null assertion.

* fix(review): unguessable heredoc delimiter for the report output

The report embeds the manifest `hold` field (fork-PR-editable); a fixed
DRIFT_EOF delimiter in a hold value could close the $GITHUB_OUTPUT heredoc
early and inject output keys. Use DRIFT_EOF_$(openssl rand -hex 16) — a value
the report cannot contain. (Randomized delimiter over base64: keeps REPORT raw
markdown, no consumer-side decode.)

* fix(review): scope issues:write to scheduled runs (two-job split)

GitHub Actions has no step-level permissions, so the only way to keep PR runs
(incl. forks) from receiving `issues: write` is to split the job. A `report`
job (contents:read, all events) renders the report + the PR `:⚠️:` and
exposes report/exit_code as job outputs; a schedule-only `upsert-issue` job
(needs: report, issues:write, no checkout) consumes them for the issue upsert +
close. The 'Check upgrade readiness' check name is preserved.

* fix(review): launder npm-version '?' in disposition prose

The disposition bucket prose interpolated r['npm_version'] raw, so a successful
200 npm /latest response lacking a 'version' key would render a bare '?' (the
matrix cell already laundered it). Add npm_version_label ('unknown' for '?') and
use it in all five bucket renderers. Test a version-less npm response.

* refactor(review): load_vendored_manifest returns only the consumed 'hold'

The readiness script reads only the grammar names + 'hold'; the 'key' and
'upstream' fields were phantom data (upstream-drift coords live in the script's
own GRAMMARS map). Narrow the return to {hold}.

* fix(review): unify detect()/apply() 'newer' check for github grammars

detect() compared the bare sha7 while apply() compared up.version (the full
<base>-g<sha7> provenance string apply() also writes). After the bot re-vendored
a github grammar once, detect() reported a perpetual false 'update available'
while apply() correctly saw 'already current' — a noisy job summary + wasted
--apply subprocess (the PR-exists guard absorbed it before any duplicate PR).
Extract a shared isNewer(up, have) helper used by both. Tests cover equal-
provenance (false), first-vendoring plain-version (true, not suppressed), and
sha-advanced (true). Coupled with U12 (the detect⇄apply agreement assertion
lives there once apply()'s not-newer path returns instead of process.exit).

* test(review): cover main()'s out-of-range + prebuilt-only vendored ABI branches

main()'s vendored-ABI classification reads through vendored_abi_from_repo (the
local-read seam --assert-current uses), so patching it drives the
'Vendored (ABI out of range)' blocker branch and the prebuilt-only (vendored_abi
None → 'prebuilt' cell, not '?') branch — neither reachable today since all 5
vendor dirs ship parser.c at ABI 14.

* test(review): monitor-side manifest⇄vendor-dir consistency guard

Mirror the Python consistency guard on the monitor side — the monitor consumes
the same manifest and is the side that WRITES files from manifest `name`, so
manifest/vendor-dir drift must fail CI here too.

* fix(review): validate grammar names at manifest load (path-traversal guard)

The manifest `name` is joined into gitnexus/vendor/<name> paths in both scripts
(and apply() WRITES there), so reject any name not matching tree-sitter-[a-z0-9-]+
at the single load chokepoint — defense-in-depth even though the live trust
boundary already prevents exploitation. loadManifestGrammars gains an injectable
`raw` arg + export for testing; tests reject a '../etc' name in both scripts.

* refactor(review): apply() throws ApplyExit; CLI maps to exit codes

apply()'s 4 process.exit calls killed the vitest worker, blocking in-process
tests of its error branches. Replace them with a thrown ApplyExit{code}; the
not-newer (already-current) path returns `have` instead of exit(0). The isMain
CLI block try/catches and maps ApplyExit.code → process.exit, so the monitor's
subprocess contract (exit 0/2/3) is byte-identical (verified via subprocess
smoke). Tests cover unknown-key=2, held=3, ABI-reject=3, and not-newer (returns
current, no throw, no write).

* refactor(review): extract vendored render helper; trim docstrings (<1000 lines)

Extract the 'Vendored parsers' prose render into _render_vendored_section() so
main() coordinates named phases rather than inlining a ~450-line monolith, and
condense the most verbose docstrings/comments. The script drops from 1092 to 999
lines (under the 1000 bar the maintainability review flagged). Behavior-preserving:
the deterministic --offline render is byte-identical before/after (verified
in-place), --assert-current still passes, and the full unit suite is green.

* fix(review): row-diff regex captures only the Status cell

The change-detection regex captured the whole row tail as group 2, so any
non-status cell drift (e.g. an upstream-ABI bump) emitted a false-positive
'change' line. Capture only the Status cell ([^|]+? before the final |$).
The workflow parseRows regex and the Python _ROW_DIFF_RE stay byte-identical;
the stability test now asserts group 2 is the status string (e.g. c →
'Vendored — held') and contains no pipe.

* fix(ci): hoist intro string out of the list literal (CodeQL 755)

The U13 extraction moved the 'Vendored parsers' intro paragraph (implicitly
concatenated string literals) INTO a list literal, tripping CodeQL
py/implicit-string-concatenation-in-list (reads as a possibly-missing comma
between elements). Hoist it into a parenthesized `intro` variable. Render is
byte-identical.
2026-06-13 16:15:49 +01:00
dependabot[bot]
1150eea98f
chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#2152)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...df4cb1c069)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-10 21:58:49 +01:00
dependabot[bot]
ae1ec82f5f
chore(deps): bump actions/attest-build-provenance from 2.4.0 to 4.1.0 (#2158)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v2.4.0...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 21:55:17 +01:00
dependabot[bot]
5a590f052f
chore(deps): bump docker/setup-qemu-action from 4.0.0 to 4.1.0 (#2159)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](ce360397dd...06116385d9)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 21:55:03 +01:00
dependabot[bot]
df56d34f33
chore(deps): bump release-drafter/release-drafter from 7.3.0 to 7.3.1 (#2157)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](c2e2804cc5...693d20e7c1)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 21:52:55 +01:00
dependabot[bot]
792cd96d37
chore(deps): bump actions/setup-python from 5.6.0 to 6.2.0 (#2155)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.6.0...a309ff8b426b58ec0e2a45f0f869d46889d02405)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 21:52:24 +01:00
Gergő Magyar
5bf8a17cd5
feat(ingestion): add control-flow-graph layer for TS/JS (#2081) (#2099)
* feat(cfg): language-agnostic CFG construction core (#2081)

U1 of M1 (CFG layer). Plain JSON-serializable CFG data model (BasicBlockData/
CfgEdgeData/FunctionCfg — must survive the worker→main boundary + ParsedFile
store), a CfgBuilder accumulator (leaders→blocks→edges, synthetic ENTRY/EXIT,
idempotent edges), a ControlFlowContext (break/continue/switch + labeled-jump
target stacks), and a TraversalResult ({entry, dangling exits}). AST-agnostic
and unit-tested on the classic control-flow topologies (if/else, while back-edge,
mid-block return, labeled break/continue) the S2 spike validated; reachability
helper backs the R9 property test.

* feat(ingestion): U2 — TS/JS CFG visitor over tree-sitter AST (#2081)

Add the TS/JS CfgVisitor that walks a function's tree-sitter AST and drives
the U1 CfgBuilder to produce a serializable FunctionCfg. One visitor covers
both languages (shared grammar family).

Handles the classic CFG hazards explicitly (R2, R10):
- loops allocate a dedicated loop-exit block so `break` has a concrete target
  before the loop's successor is known; `continue`/back-edge close the loop
  (while, do-while, C-for with init-once + increment-as-continue-target,
  for-in, for-of)
- switch fallthrough falls out naturally: a non-breaking case yields exits we
  wire to the next case as `fallthrough`; a breaking case wires to the switch
  exit via ControlFlowContext
- try/catch/finally: normal completion AND exceptional flow both route through
  finally (post-domination); a conservative exceptional edge models that the
  protected region may raise to its handler (not just explicit `throw`)
- labeled break/continue resolve against the labeled loop's frame
- early return/throw wire to EXIT/handler and terminate their block

19 hazard tests (one per construct) + AC1 10-function fixture; all green.
No change to the committed U1 core or ControlFlowContext.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ingestion): U3 — worker CFG build + cfgSideChannel + cache coherence (#2081)

Run the CFG visitor in the parse worker (where the AST lives), serialize the
per-function CFG onto a new ParsedFile.cfgSideChannel, and keep it coherent
across the disk-backed store and the warm/durable parse cache (R3, R4).

- gitnexus-shared parsed-file.ts: add `cfgSideChannel?: unknown` as a DISTINCT
  field from captureSideChannel (different producer/consumer/lifecycle; plain
  JSON data — blocks/edges deliberately lack the `nodeId` the store's interning
  reviver keys on, so no mis-interning).
- cfg/types.ts + visitors/typescript.ts: add CfgVisitor.isFunction so the worker
  enumerates functions (and applies the line budget) by a cheap node-type test.
- cfg/collect.ts (new): collectFunctionCfgs walks the tree, builds one CFG per
  function (nested included), applies maxFunctionLines (over-cap = skipped).
- language-provider.ts: add `cfgVisitor?: CfgVisitor<SyntaxNode>` hook;
  typescript.ts attaches it to both the TS and JS providers (shared grammar).
- parse-worker.ts: read pdg + pdgMaxFunctionLines from workerData (read once at
  init — the worker never sees PipelineOptions), gate the build, attach
  cfgSideChannel alongside captureSideChannel.
- parse-cache.ts: bump SCHEMA_BUMP 4→5 (ParsedFile shape changed) and fold the
  pdg flag into computeChunkHash so a pdg-off cached chunk is NOT reused on a
  --pdg run (the #2038-class warm-cache trap). Default path keeps its keys.
- worker-pool.ts + parse-impl.ts + pipeline.ts: thread pdg/pdgMaxFunctionLines
  PipelineOptions → WorkerPoolOptions → workerData, and into the chunk-hash key.

9 boundary tests: collect contract, JSON round-trip identity (no AST leakage),
the pdg cache-key guard, the line-cap skip, and the no-visitor gate. Full CFG
suite (U1+U2+U3) green; build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ingestion): U4 — emit BasicBlock + CFG within scope-resolution (#2081)

Emit persisted BasicBlock nodes + CFG edges from each ParsedFile's worker-built
cfgSideChannel, INSIDE scope-resolution's Phase-4 graph emission — the last
point where the worker-built CFGs are loaded (emitParsedFiles carries the
channel; the disk store is cleared right after the orchestrator returns). This
is the architecture the doc-review corrected to: a standalone post-`mro` phase
(the issue's literal subtask) provably reads empty data (KTD1).

- cfg/emit.ts (new): pure emitFileCfgs(graph, cfgs, maxEdgesPerFunction, onWarn).
  BasicBlock id = `BasicBlock:<filePath>:<functionStartLine>:<blockIndex>`
  (KTD3 — funcStart disambiguates blocks across functions in one file; no
  `name` column). CFG edge = CodeRelation type 'CFG' with the edge KIND
  (seq/cond-true/…) in `reason` (kinds can't be their own edge type). Per-
  function edge cap stops at the cap and warns with the dropped count — no
  silent truncation (R6/KTD6).
- run.ts: pdg-gated emit pass over emitParsedFiles after emitPostResolutionEdges
  (store still live); RunScopeResolutionInput gains pdg + pdgMaxEdgesPerFunction.
- phase.ts: thread ctx.options.pdg / pdgMaxEdgesPerFunction into the call.
- pipeline.ts: PipelineOptions.pdgMaxEdgesPerFunction.

6 tests: node/edge shape (KTD3 id, no name, type='CFG', kind in reason),
cross-function id uniqueness, AC2 reachability-from-ENTRY property, the edge
cap's no-silent-truncation contract, and empty-input no-op. Flag-off
byte-identity + full runPipelineFromRepo round-trip land in U7. Build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): U5 — `--pdg` opt-in plumbing (CLI + .gitnexusrc → both sinks) (#2081)

Expose the CFG/PDG substrate as an opt-in and thread it from CLI/.gitnexusrc to
the single source of truth (PipelineOptions.pdg), which fans out to BOTH sinks
already wired in U3/U4: the worker build gate (workerData.pdg) and the
scope-resolution emit gate. Off by default (R7).

- cli/index.ts: `--pdg` commander flag.
- cli/analyze.ts: AnalyzeOptions.pdg + pass `pdg` into runFullAnalysis options.
- cli/analyze-config.ts: KEY_SPECS `pdg` (boolean) so `.gitnexusrc { "pdg": true }`
  normalizes and a non-boolean value fails closed with GitNexusRcError.
- core/run-analyze.ts: AnalyzeOptions.pdg → runPipelineFromRepo({ pdg }).

(The internal PipelineOptions/WorkerPoolOptions/workerData fields + the
parse-cache key fold landed in U3/U4; this unit adds the user-facing surface.
The budget knobs stay at internal defaults for M1.)

Tests: analyze-config pdg normalization + non-boolean rejection; opt-in.test.ts
covers the CLI/file merge precedence and that pdg perturbs the chunk-dispatch
key. The full worker-build + main-emit round-trip is the U7 integration test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ingestion): U7 — CFG acceptance fixtures, parity, end-to-end + docs (#2081)

Acceptance criteria for the M1 CFG layer:
- AC1: a 10-function TS fixture's CFG node/edge set matches a committed snapshot
  (cfg-snapshot.test.ts).
- AC2: every BasicBlock is reachable from its function ENTRY (property test over
  the emitted graph; the fixture has no dead code).
- AC3: hazard fixtures lock the classic-bug coverage — try/throw/finally
  post-domination + labeled break/continue resolution.
- AC4: the existing pipeline-graph-golden test stays byte-identical with --pdg
  off (verified; no UPDATE_GOLDEN), proving the opt-in adds zero default-run
  drift.
- End-to-end (pipeline-pdg.test.ts): runPipelineFromRepo({ pdg: true }) on a
  tiny repo emits BasicBlock nodes + CFG edges with both endpoints present —
  the true both-sinks proof (worker builds → store → scope-resolution emits);
  the default run emits zero.

Docs: CHANGELOG M1 entry, ARCHITECTURE "Optional CFG/PDG emission" subsection
(why emit is in-phase, not post-mro), README CFG language-support note.

Full CFG suite (U1–U7): 56 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ingestion): drop unused helper in cfg-snapshot test (#2081)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply ce-code-review autofix feedback (#2081)

Review (10 reviewers) confirmed OFF-path byte-identity (adversarial + golden)
and found defects all within the --pdg path. Fixes:

- P1 same-line BasicBlock id collision: add a start-column disambiguator to
  FunctionCfg + the id (`BasicBlock:<file>:<line>:<col>:<idx>`) so two functions
  sharing a start line no longer collide under first-writer-wins addNode.
- P1 worker crash-cascade: per-file try/catch around collectFunctionCfgs so a
  CFG-build throw cannot escape to the language-group catch and silently drop
  every remaining file in the group.
- P2 edge-cap drop now logs unconditionally (input.onWarn is validator-gated/
  silent in prod) — upholds the no-silent-truncation guarantee.
- P2 Array.isArray guard before the cfgSideChannel cast in run.ts.
- P2 maxFunctionLines default: worker applies DEFAULT_PDG_MAX_FUNCTION_LINES=2000
  when unset; caps forwarded through run-analyze AnalyzeOptions (closes the
  server-path drop).
- P3 README duplicate paragraph removed; `0`-vs-default docstrings corrected;
  CLI --pdg flag made language-neutral; reachableBlocks JSDoc corrected.
- Documented the break-through-finally + stacked-label CFG limitations.
- Tests: same-line id-collision regression, standalone throw→EXIT, dead-code-
  after-return, async/generator/method coverage, strengthened labeled-continue.

Refuted: the HTTP-500 getNodeQuery finding — M0 already shipped the BasicBlock
branch + name-floor (R12/web-safety handled).

CFG + analyze-config suites: 95 tests green; golden parity (AC4) byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(ingestion): benchmark CFG construction + O(n) block-text accumulation (#2081)

Closes the M1 review's requires_verification perf gap ("no benchmark for
collectFunctionCfgs; a wall-time + cfgSideChannel byte-size regression gate
would catch the extendBlock concatenation before kernel scale").

- bench/cfg/measure.mjs (new): build-free tsx harness timing collectFunctionCfgs
  (parse once, reuse the tree) across three scaling scenarios — straight-line
  (extendBlock path), many-functions (collect walk), branchy (block/edge growth)
  — at 500→2000. Reports a wall-time scaling ratio AND a cfgSideChannel
  byte-size ratio, plus an order-independent sha256 over the emitted blocks/edges
  as the behavior gate. `--check` compares both ratios + the fingerprint against
  bench/cfg/baselines.json; mirrors the scope-capture / python-scope harnesses.
- .github/workflows/ci-tests.yml: run the gate on every test job (build-free,
  alongside the existing scope-capture guards) so an O(n^2) re-regression fails CI.
- cfg-builder.ts: structural fix for the one real hotspot the bench surfaced —
  accumulate basic-block text as fragments joined once in finish(), instead of
  concatenating onto a growing string per coalesced statement (O(n^2) → O(n)).
  Behavior-identical (the CFG fingerprint + the AC1 snapshot are unchanged).

Measured (post-fix): time ratios straight-line ~1.3, many-functions ~1.0,
branchy ~1.1 (all sub-quadratic; a true O(n^2) would be ~4.0). cfgSideChannel
bytes scale linearly (~1.0-1.04). 60 CFG tests green; build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(ingestion): add memory + disk growth gates to the CFG benchmark (#2081)

Extend bench/cfg/measure.mjs beyond wall-time to the two other scalability
dimensions that matter at kernel scale:

- DISK growth: utf8 byte size of the serialized cfgSideChannel — exactly what a
  --pdg run writes onto every ParsedFile shard (durable store + parse cache).
- MEMORY growth: retained JS heap of the cfgSideChannel payload, measured by the
  release-delta method (heap held minus heap after dropping it) — robust to
  pre-existing garbage and dead-stable run-to-run. Needs `node --expose-gc`;
  without it the heap metric is null and its gate is skipped (local runs still
  work). ci-tests.yml now passes --expose-gc so the heap gate runs in CI.

Both gated on linear scaling in baselines.json (disk_bytes_budget / heap_budget
1.2-1.3). Measured: disk ~1.0-1.04, retained heap ~0.87-1.0 — both linear
(~1KB/function each; ~2MB heap / 1.6MB disk at 2000 functions, --pdg only).
Bumped REPS 7->15 to stabilize the noisier time signal and widened the coarse
time tripwire budgets (the disk/heap gates carry the tight regression detection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ingestion): address tri-review + CFG-expert findings (#2081)

Corroborated findings from the tri-review (Codex + CE personas + GitNexus swarm
+ a CFG/program-analysis domain-expert lane). The OFF-path stays byte-identical;
all fixes are within the --pdg path or the benchmark.

- [Codex+CFG-expert] Exceptional `throw` edges now wire EVERY block in a try's
  protected region to the handler, not just the body ENTRY. A branched try body
  (`try { if (x) { use(t); } } catch`) previously left interior blocks with no
  path to `catch` — a taint false-negative into the handler for the M2 PDG pass.
- [Codex+CFG-expert] An unresolved labeled jump (a stacked outer label or a
  labeled non-loop block) now routes to the function EXIT instead of leaving a
  dangling sink — restores the single-exit invariant post-dominator/PDG
  computation needs.
- [Codex] computeChunkHash now folds pdgMaxFunctionLines/pdgMaxEdgesPerFunction
  into the chunk key (not just the pdg boolean), so a warm cache built under one
  cap is never served to a run with a different cap (#2038 class, extended to
  the budgets). Adds PdgCacheKey; boolean form kept for back-compat.
- [perf] visitTry resolves catch/finally in a single namedChild pass (the double
  `namedChildren.find` allocated two throwaway arrays).
- [adversarial] The bench `straight-line` scenario now runs at 2000->8000:
  output is a constant 4 blocks so disk/heap can't see the concat path, and at
  the old N a genuine O(n²) was masked by V8 cons-strings. Verified at the new N:
  the array-join impl ~1.0, a rope-optimized `+=` ~1.0 (correctly not flagged),
  a real O(n²) (re-join-every-append) ~3.8 — budget tightened 2.0->1.5.
- [adversarial+Codex] The bench `--check` now FAILS LOUDLY when run without
  `--expose-gc` instead of silently skipping the retained-heap gate.
- Doc: re-labeled the finally-bypass as a SOUNDNESS (false-negative) limitation
  tracked for M2, not mere "precision."

3 new regression tests (branched-try interior→handler, stacked-label→EXIT,
cap-fold key). 99 CFG tests pass; build clean; bench gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(parse-cache): clarify that SCHEMA_BUMP still invalidates caches once (#2099 F6)

The computeChunkHash comment claimed pdg-off warm caches "survive this
change untouched" — true for the key FORMAT, but misleading as an
upgrade-behavior promise: SCHEMA_BUMP 4→5 changes PARSE_CACHE_VERSION
and both stores hard-invalidate on it. Separate the two facts so the
next cache change isn't reasoned about from a false premise.

Review finding F6 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): correct for-loop back-edge kinds when no increment clause (#2099 F5)

A for with a body but no increment emitted an unconditional
header→header 'loop-back' self-edge (a path that never executes the
body) while the real back-edge body→header was labeled 'seq'. Any
consumer identifying loops via reason='loop-back' picked the phantom
edge and excluded the body from the natural loop.

Gate the self-edge on the body being absent (the one case where the
header genuinely re-tests itself) and carry 'loop-back' on the body's
exits when they ARE the back-edge, matching visitWhile/visitForIn.

Review finding F5 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): treat an empty catch clause as a real handler (#2099 F2)

visitTry keyed handler semantics off the traversal result — null for an
empty body, since visitSeq([]) returns null — instead of the syntactic
clause. An empty `catch {}` was therefore treated as NO catch: the
swallowed exception escaped to the outer handler/EXIT, the no-catch
re-propagation misfired past finally, and code after a try whose body
always throws became unreachable from ENTRY — a hard false-negative
source for the M2 taint pass, on an extremely common pattern.

Synthesize one empty block spanning the clause (entry == sole exit)
when the catch body traverses to null, before the protected region is
walked. Exception flow lands in it and rejoins the normal continuation;
all downstream wiring (handler selection, finally routing, the !catchRes
re-propagation gate) operates on the syntactically-correct shape.

Review finding F2 (P2, reproduced) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cfg): guard CFG emission per element, not just per outer array (#2099 F4)

The cfgSideChannel guard checked only Array.isArray before casting to
FunctionCfg[] — its own comment promised a wrong-shape value would
'skip emission, not throw a TypeError mid-graph-build', but a malformed
ELEMENT sailed through. Worse, the obvious-looking failure shape never
throws at all: emitFileCfgs string-templates any edge endpoint into the
BasicBlock id and graph inserts are no-throw, so a non-integer endpoint
silently became a dangling 'BasicBlock:…:undefined' edge that degrades
the DB rel-pair COPY to row-by-row fallback inserts much later.

Layered fix matching house precedents (parsedfile-store reviver,
worker-side per-file catch): a per-element shape+content predicate
(arrays + integer edge endpoints) that warns and skips malformed
elements while valid siblings still emit, plus a per-file try/catch
backstop for shapes that genuinely throw (e.g. a null inside blocks).

Review finding F4 (P3) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(parse-cache): drop emit-time edge cap from the pdg chunk key (#2099 F3)

pdgMaxEdgesPerFunction is applied exclusively in emitFileCfgs during
scope-resolution on the main thread — the worker never receives it
(workerData carries only pdg + pdgMaxFunctionLines), so the cached
worker output is byte-identical across cap values. Folding it into the
chunk key (added by a prior review round) only converted a free knob
into a repo-sized cost: every cap change forced a full re-parse and a
durable-store rewrite of unchanged data.

Keep pdg + maxFunctionLines (genuinely worker-visible, shape the cached
cfgSideChannel) and document the classification test in the PdgCacheKey
doc comment so the next option gets sorted deliberately: worker-shard
inputs go in this key; persisted-graph-only inputs belong in the
RepoMeta pdg stamp (F1). Chunks written under the old ns string miss
once and prune — no migration needed.

Review finding F3 (P2) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(analyze): record pdg config in RepoMeta; force full writeback on mode flip (#2099 F1)

Running --pdg against an already-indexed repo silently persisted ~zero
CFG: incremental eligibility had no pdg term, RepoMeta recorded no
mode, and extractChangedSubgraph keeps only changed-file nodes — on a
no-change --pdg re-run every freshly built BasicBlock was dropped from
the written subgraph ('Incremental: changed=0', run succeeds, zero
rows). The converse flip left zombie mixed-coverage blocks only --force
could clean. Worse, a clean-tree flip hit the alreadyUpToDate fast path
and never ran the pipeline at all.

- RepoMeta gains an additive-optional pdg stamp ({maxFunctionLines,
  maxEdgesPerFunction}, resolved values; absent ≡ pdg-off, which covers
  every legacy meta). No INCREMENTAL_SCHEMA_VERSION bump — that would
  force a one-time full rebuild for everyone. The end-of-run meta is a
  fresh literal, so omitting the field on a pdg-off run is what clears
  the stamp after an on→off flip.
- pdgModeMismatch (pure, exported) compares the resolved triple; the
  flip check sits before the fast path and always logs its notice (not
  gated on options.force — --skills implies force with no message of
  its own), naming the .gitnexusrc pdg key that pins the mode.
- The full-rebuild branch now writes the incrementalInProgress dirty
  flag (toWriteCount: 0 sentinel) before the wipe whenever a prior meta
  exists, mirroring the incremental branch. This closes the crash
  window where a rebuild dying between the bulk load and saveMeta left
  meta/DB inconsistent and the fast path certified zombie (or missing)
  CFG rows indefinitely — and incidentally closes the same pre-existing
  hole for user --force runs. Recovery log reworded accordingly.

Tests: pdg-mode-flip.test.ts (real git + LadybugDB; primary assertion
is a direct BasicBlock table count — meta.stats aggregates
nondeterministic Community/Process rows) covering off→on, steady-state
fast path, on→off zombie cleanup, cap-change rebuild, and dirty-flag +
flip composition; pure-helper tests for default resolution and the
0=unlimited carve-out.

Review finding F1 (P1) of PR #2099 tri-review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:26:45 +01:00
Gergő Magyar
247ea431de
fix(ci): make the prebuild PR push re-run-safe (--force-with-lease -> --force) (#2123) 2026-06-09 20:28:08 +01:00
Gergő Magyar
bd90d5bf90
fix(ci): green the tree-sitter prebuild matrix (npm-bundled prebuilds + arm64 runtime) (#2122)
First real dispatch of build-tree-sitter-prebuilds failed every job from two
independent root causes:

1. `c` (kind:'npm'): tree-sitter-c's npm tarball bundles prebuilds/ for all 6
   tuples, so the post-build `find ... -print -quit` picked a non-host tuple
   (win32-x64 on a linux runner) and the "built X, expected Y" assertion failed.
   Clear $pkgdir/prebuilds before prebuildify so only the freshly-built host
   tuple remains. (kotlin is npm too but ships no prebuilds, so it dodged this.)

2. every linux-arm64: validate installed tree-sitter@0.21.1 with
   --ignore-scripts, but that tarball ships no linux-arm64 prebuild, so
   require("tree-sitter") threw "No native build was found ... arch=arm64". The
   grammar's own arm64 .node loaded fine. Drop --ignore-scripts and add node-gyp
   + node-addon-api so the runtime source-builds where upstream ships no prebuild;
   prebuild-covered tuples still use the prebuild. The grammar-vs-runtime ABI
   check still fires at setLanguage.
2026-06-09 19:39:10 +01:00
Gergő Magyar
6ab3f64443
fix(ci): drop the broken -t 22 from prebuildify (build-tree-sitter-prebuilds) (#2121)
The native build step ran `prebuildify --napi --strip -t 22`, but prebuildify
parses the bare `-t 22` as the NUMBER 22 and crashes in resolveTargets
(`TypeError: v.indexOf is not a function`) — so every matrix job (c/dart/proto/
kotlin × 6 tuples) failed on its first real run. N-API prebuilds are
Node-version-agnostic, so `-t <node-version>` is both wrong and the cause; drop
it. Verified locally: `prebuildify --napi --strip` builds the vendored c source
cleanly into prebuilds/<tuple>/tree-sitter-c.node and exports
napi_register_module_v1.
2026-06-09 19:02:06 +01:00
Gergő Magyar
cef63dd044
feat(install): toolchain-free tree-sitter via vendored prebuilds (#2113)
* feat(install): toolchain-free tree-sitter via vendored GitNexus-built prebuilds

Eliminate the C/C++-toolchain requirement at install for the at-risk grammars
(dart, proto, kotlin) by generating + vendoring native prebuilds, mirroring the
existing vendored tree-sitter-swift. The 10 grammars that already ship 6 upstream
prebuilds stay npm dependencies (toolchain-free AND dependency-review-tracked).

- .github/workflows/build-tree-sitter-prebuilds.yml: a registry-parameterized
  workflow that builds {dart,proto,kotlin} x {linux,darwin,win32}-{x64,arm64}
  prebuilds natively, validates each loads + parses on its arch, and opens a PR
  vendoring them. A `guard` job gates the heavy matrix to run ONLY on dispatch
  or a real grammar-version change — ordinary code PRs cost zero matrix minutes.
- dart/proto: prefer a committed prebuild; fall back to today's source build
  when none matches (no behavior change until prebuilds are vendored).
- kotlin: vendor it (Swift parity) instead of compiling the third-party
  optionalDependency from source at the user's install — supersedes #2110's
  optionalDependency mechanism. The ~23 MB parser.c is NOT vendored (the
  workflow builds from the published package); only node-types + bindings +
  prebuilds are. Removed from optionalDependencies; lock regenerated; probe,
  parser-loader note, README/.devcontainer docs, and the #2110 tests updated.

DO NOT MERGE until vendor/tree-sitter-kotlin/prebuilds/ is populated by the
build-tree-sitter-prebuilds workflow: until then Kotlin is unavailable (vendored
with no source-build fallback). dart/proto remain fully functional throughout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(install): guard 6/6 N-API prebuild coverage for every grammar

Regression guard so a toolchain-less install can never silently lose a tree-sitter
language on a supported platform-arch:

- Vendored grammars (vendor/tree-sitter-*): every one MUST ship a loadable N-API
  prebuild for all 6 tuples {linux,darwin,win32}-{x64,arm64}. Asserts the
  napi_register_module_v1 entry symbol in each .node (cross-platform, no need to
  run the binary). Currently RED for dart/proto/kotlin until the
  build-tree-sitter-prebuilds workflow populates their prebuilds/ — this is the
  must-fill-before-merge gate (swift already passes 6/6).
- npm-dependency grammars: asserts upstream ships 6/6 N-API too, catching a
  future platform drop. tree-sitter-c is allow-listed at 4/6 (missing
  linux-arm64/win32-arm64) pending #2116; the guard also fails if that gap is
  silently closed (prompting allow-list removal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(install): vendor tree-sitter-c at 0.21.4 with GitNexus-built prebuilds (#2116)

tree-sitter-c is the one grammar dependency upstream ships incomplete prebuilds
for (4/6 — no linux-arm64/win32-arm64), AND it is a REQUIRED grammar: its own
`install` (node-gyp-build) compiles from source when no prebuild matches and
exits non-zero, so on a toolchain-less ARM host `npm install gitnexus` HARD-FAILS
at the c step — during npm's dependency phase, before any GitNexus postinstall
runs (so a postinstall "supplement" can't help).

Fix: vendor c prebuild-only at the pinned 0.21.4 (Kotlin pattern), with all six
prebuilds GitNexus-cross-built, and drop it from `dependencies`:
- vendor/tree-sitter-c/ (bindings + node-types + manifest + prebuilds); build
  probe scripts/build-tree-sitter-c.cjs; added to the build workflow registry
  (kind 'npm' — built from c@0.21.4 source).
- materialize-vendor-grammars.cjs: c is REQUIRED, so it is always materialized,
  even under GITNEXUS_SKIP_OPTIONAL_GRAMMARS (it needs no toolchain).
- Removed from package.json dependencies + lockfile (nothing else needs npm c —
  tree-sitter-cpp's dep on c is dev-only and not installed). Preserves the #1242
  ABI pin: vendoring 0.21.4 keeps the good ABI while closing the ARM gap.
- parser-loader note + the prebuild-coverage guard + a cli-commands assertion
  updated; c moves from the npm-gap allow-list into the vendored 6/6 cohort.

Verified: tsc clean, 31 unit tests pass, c loads/parses; the guard is RED for
c/dart/proto/kotlin until the workflow populates prebuilds (the must-fill gate).
Closes the operational risk in #2116.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): source-build fallback for vendored c/kotlin so CI is healthy pre-prebuilds

The vendored prebuild-only grammars (c, kotlin) had empty prebuilds/ until the
build-tree-sitter-prebuilds workflow runs, so they could not load in CI — and
C is hard-required by cross-platform tests (tree-sitter-languages/parsing on
ubuntu+macos+windows), which I cannot pre-build for macos/windows locally. The
robust fix is a source-build fallback that works on every CI runner (all have a
toolchain), mirroring dart/proto:

- Vendor the grammar source (binding.gyp + src/) for c and kotlin; their build
  scripts now PREFER a committed prebuild (toolchain-free) and fall back to
  `node-gyp rebuild` from the vendored source when no prebuild matches. Verified
  both compile against the hoisted node-addon-api@^8 and the runtime loads.
- prebuild-coverage guard is now bootstrap-tolerant: a grammar that vendors its
  source (binding.gyp) may have an incomplete prebuild set (the workflow fills
  it); a prebuild-only grammar (swift) still must ship all six. Any present
  prebuild must still be N-API. Guard goes green; it re-tightens per-grammar as
  the workflow populates prebuilds.
- actionlint: silence a false-positive SC2016 (JS template literals inside the
  single-quoted `node -e` validate block).

Note: kotlin's generated parser.c is large (~23 MB on disk; compresses heavily
in git). Once the workflow populates all six kotlin prebuilds, the source serves
only as the fallback and could be slimmed if desired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(docker): re-materialize+rebuild vendored grammars after npm prune

`npm prune --omit=dev` in the gitnexus CLI image drops anything not in
package.json's dependency tree — including the VENDORED tree-sitter grammars
(materialized by postinstall, not declared deps) and their built bindings. The
`serve` image analyzes/parses repos at runtime, so re-run the grammar postinstall
after the prune (in the toolchain-equipped builder) to restore them. Load-bearing
for tree-sitter-c, a core REQUIRED grammar now vendored (#2116): as a former
dependency it survived prune; vendored, it would not. Also restores
swift/dart/proto/kotlin, which were silently pruned from the image before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(grammars): unify tree-sitter-swift with the vendored-source build pipeline

Swift was the last grammar handled differently — it shipped only upstream
prebuilds, while c/dart/proto/kotlin vendor their grammar source and use a
prefer-prebuild -> source-build-fallback activation script. Vendor swift's
source so all five are handled identically (one uniform build path).

- vendor/tree-sitter-swift: add binding.gyp (win-hardened), bindings/node/
  binding.cc, src/parser.c (ABI-14 default, ~18 MB), src/scanner.c, and
  src/tree_sitter/ headers. The 6/6 prebuilds are retained. The legacy
  parser_abi13.c alternate is intentionally not vendored.
- build-tree-sitter-swift.cjs: rewrite the prebuild probe into the dart-style
  prefer-prebuild then source-build fallback (keeps the GITNEXUS_SKIP gate and
  the never-exit-non-zero postinstall invariant).
- build-tree-sitter-prebuilds.yml: register swift (kind 'vendored'); add its
  package.json to the version-gated pull_request paths and a validate snippet.
- prebuild-coverage guard auto-moves swift into the source-fallback cohort
  (binding.gyp now present); refresh the stale "swift is prebuild-only" comments.
- tests: add build-tree-sitter-swift-probe.test.ts; fix the pre-existing
  build-tree-sitter-kotlin-probe.test.ts breakage (it still asserted the old
  probe strings after kotlin's dart-style conversion); assert swift's vendored
  source in cli-commands.test.ts.
- docs: README / .devcontainer / kotlin vendor README — swift's prebuilds are
  now GitNexus-cross-built from vendored source like the rest, not upstream-only.

Verified: swift source-builds against node-addon-api@8 -> N-API binary -> loads
against the pinned tree-sitter@0.21.1 (ABI 14) -> parses cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(publish): gate a lean prebuilds-only npm tarball behind a coverage guard

Vendoring grammar source (parser.c) alongside the prebuilds means the npm
tarball now carries ~50 MB of generated source it almost never compiles (every
supported platform-arch has a prebuild). Prepare to drop it from the published
package once all prebuilds exist — safely.

- .npmignore: add a GATED, commented-out "lean publish" block that excludes the
  source-build inputs (parser.c/scanner.c/tree_sitter/binding.gyp/binding.cc) but
  keeps prebuilds/ + the runtime files. Uncommenting ships prebuilds-only.
- scripts/assert-publish-grammar-coverage.cjs: a prepack guard that refuses to
  pack/publish if the source exclusion is active while any vendored grammar still
  lacks 6/6 prebuilds (which would ship a grammar with no loadable binding). Wired
  into `prepack` (runs on npm pack + publish, incl. the publish.yml dry-run) and
  exposed as `npm run assert-publish-coverage`.
- test: pure-core decision cases + a real-repo publish-safety check that fails CI
  if .npmignore is activated prematurely.

Net: the prebuilds already publish today (files: ["vendor"]); this makes the
future switch to a prebuilds-only tarball a one-line uncomment that can't ship a
dead grammar. The guard currently reports "source + prebuilds" (only swift has
6/6 prebuilds so far) and passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(grammars): consolidate the 5 build-tree-sitter-*.cjs into one

The per-grammar activation scripts (c/dart/proto/swift/kotlin) were ~95%
identical — same prefer-prebuild → source-build → never-fail flow, differing only
in name, target_name, required-vs-optional, and the display label in warnings.

- scripts/build-tree-sitter-grammars.cjs: one registry-driven script. Bare call
  builds all (postinstall); `... <name>` builds only the named grammars (so the
  probe test can isolate one). c is `required: true` (ignores the opt-out gate);
  the rest honor GITNEXUS_SKIP_OPTIONAL_GRAMMARS. Per-grammar try/catch + a final
  process.exit(0) preserve the postinstall never-exit-non-zero invariant.
- package.json: postinstall is now `materialize && build-tree-sitter-grammars.cjs`
  (was five chained `build-tree-sitter-<name>.cjs` calls).
- tests: replace the two near-identical *-probe.test.ts files with one
  parameterized build-tree-sitter-grammars-probe.test.ts that also covers the
  required-vs-optional opt-out split and an unknown-grammar arg.
- update cli-commands.test.ts postinstall assertions + the vendor c/kotlin/swift
  README + swift provenance to reference the consolidated script.

Behavior is preserved (warnings normalized to one consistent format). Removes 5
scripts + 1 test file; adds 1 script + 1 test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ingestion): lazy-load tree-sitter-c to prevent module-load crash

tree-sitter-c is now vendored prebuild-only (#2116) with 0/6 committed
prebuilds, so on a toolchain-less or `--ignore-scripts` install C has no native
binding. Three modules loaded it via a hard top-level `import C from
'tree-sitter-c'`, which throws ERR_MODULE_NOT_FOUND at module-load — crashing
`analyze` before parser-loader's optional/severity:error degradation can run.
This is the #2091/#2093 bug class (previously fixed for swift/dart/kotlin); C was
left static because it used to be an always-present npm dependency.

- languages/c/query.ts: load via the lazy guarded getLanguageGrammar(C), mirroring
  swift/query.ts; the main-thread isLanguageAvailable filter ensures the getters
  are reached only when C is present.
- workers/parse-worker.ts: guarded `_require('tree-sitter-c')` + conditional
  languageMap spread, like swift/dart/kotlin.
- group/extractors/include-extractor.ts: guarded `_require`; getLanguageForFile
  returns null for .c/.h when absent, so C include-extraction degrades to a no-op
  (C++ unaffected).
- extend the registry-import-closure regression test (#2091/#2093) to assert C
  also loads lazily at registry static-import time.

* fix(ci): repin attest-build-provenance to the real v2.4.0 SHA

The workflow pinned actions/attest-build-provenance@bd77c077… commented
`# v2.4.0`, but v2.4.0 is e8998f94… (verified via the GitHub API); bd77c077…
is an untagged mid-stream commit, so the SLSA-attestation step ran unvetted
action code and the comment misrepresented what runs. Repin to the real
v2.4.0 commit and drop the `# PLACEHOLDER-PIN` markers on both this line and
the setup-python pin (a26af69b… is already the correct v5.6.0 — only its
comment was stale). Update the header NOTE accordingly.

* fix(ci): skip the prebuild-PR aggregate when release App secrets are absent

The aggregate job mints a GitHub App token as its first step; with
RELEASE_APP_ID/RELEASE_APP_PRIVATE_KEY unset it hard-failed AFTER a full
(up-to-6-runner) native build. Since the `secrets` context isn't available in
a job-level `if:`, the guard job now computes a `release_app` boolean output
(a step can read secrets) and emits an actionable `::notice::`; aggregate
gates on it and skips cleanly, while the build job's artifacts still upload
(run with open_pr=false for artifacts-only).

* chore(ci): drop package-lock.json from the prebuild paths filter; widen build timeout

`gitnexus/package-lock.json` changes on nearly every dependency PR, so it
fired the prebuild workflow's guard job on unrelated churn (the matrix stayed
correctly skipped — `gitnexus/package.json` already covers the transition-window
pin, so removing the lock only drops guard noise). Also bump the native build
job timeout 30 -> 45 min for headroom compiling the 23 MB kotlin / 18 MB swift
parser.c, especially under arm emulation.

* fix(ci): event-gate the aggregate open-PR condition explicitly

`inputs.open_pr` is null on pull_request events, and the prior
`inputs.open_pr != false` leg relied on GHA's direction-ambiguous null
coercion (Codex F4) to decide whether to open the prebuild PR. Gate
explicitly on the event: a non-fork pull_request that bumped a grammar
version opens the prebuild PR (the documented flow), and `open_pr` is only
consulted on workflow_dispatch — so a manual run with open_pr=false stays
artifacts-only and no event's behavior rests on coercion.

* fix(publish): validate the effective npm-pack contents in the coverage guard

The publish guard inferred "is source shipped?" from a single .npmignore toggle
line, which a partial/out-of-order edit could defeat (exclude binding.gyp but
leave parser.c → unbuildable yet "source-shipping"). It now inspects the
EFFECTIVE tarball via `npm pack --dry-run --ignore-scripts --json` (the
--ignore-scripts avoids re-entering this guard through prepack): a grammar
"ships source" only when EVERY on-disk source-build input (binding.gyp +
binding.cc + parser.c + scanner.c when present + a tree_sitter header) is
actually in the packed file list.

This also surfaced that the gated lean-publish .npmignore block was inert:
package.json's `files: ["vendor"]` allow-list overrides .npmignore for the
vendored subtree, so those exclusion lines never dropped anything. Replace the
dead toggle with documentation of the real mechanism (narrow the `files` field)
and note the guard enforces safety on the effective pack regardless of how the
slim is done.

* test(prebuild): hard-gate declared-fully-prebuilt grammars on 6/6 coverage

The strict 6/6 prebuild assertion was dormant whenever a grammar vendors source
(binding.gyp) — which is every grammar — so a dropped prebuild passed CI
silently. Add a FULLY_PREBUILT allowlist of grammars GitNexus has committed 6/6
for (today: swift); those must keep all six even with a source fallback, so
losing one now fails CI. Grammars graduate into the set as the
build-tree-sitter-prebuilds workflow lands their binaries. (The static-import
degradation smoke is covered by the registry-import-closure regression test
extended in the C lazy-load commit.)

* chore(deps): promote node-gyp-build/node-addon-api to regular dependencies

Every vendored grammar's index.js does `require("node-gyp-build")` at runtime
to load even a prebuilt .node, so node-gyp-build is runtime-load-critical (and
node-addon-api is needed for the source-build fallback). They were
optionalDependencies, surviving `--omit=optional` only via the required
tree-sitter's transitive edge — correct today but fragile. Promote both to
regular dependencies so the contract is explicit (optionalDependencies is now
empty and removed). Lock the contract with a cli-commands assertion.

* chore(vendor): add Windows cflags parity block to tree-sitter-c/binding.gyp

c's binding.gyp used an unconditional `cflags_c: ["-std=c11"]`, while
kotlin/swift gate MSVC flags behind an `OS=='win'` condition (/std:c11 /utf-8).
Inert today (no non-ASCII bytes in c's parser.c, and node-gyp ignores cflags_c
on MSVC anyway), but align the three so a future source-build fallback on
Windows behaves consistently.

* docs(agents): correct stale optional-grammar / postinstall notes

AGENTS.md still said postinstall "patches tree-sitter-swift, builds
tree-sitter-proto" and that only kotlin/swift are "optional". Update to the
vendored-uniform model: postinstall materializes the vendored grammars and
prefers a committed prebuild (source-build only when none matches); c is
required while dart/proto/swift/kotlin are optional + skippable via
GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1, with non-fatal warnings only on a
toolchain-less host with no matching prebuild.

* fix(install): preserve the backup and warn loudly on a failed materialize rollback

If renameSync(partial, dest) failed AND the rollback renameSync(backup, dest)
also failed, the grammar was left unmaterialized (node_modules/<name> missing)
with only a generic "could not materialize" warning — the recoverable backup at
<dest>.materialize-bak was unmentioned. Emit a CRITICAL warning naming the
backup path and the recovery command on that double-failure, and document that
the fail-soft catch removes only the scratch `partial`, never the `backup`
(which may be the sole recoverable copy). Never-throw / exit-0 contract intact.

* fix(publish): make the coverage guard's npm-pack inspection script-safe

The prepack guard shelled out to `npm pack --dry-run --ignore-scripts --json`,
but the `--ignore-scripts` flag is not reliably honored by npm pack's
prepare/prepack lifecycle on the CI npm — so build.js ran, polluted the --json
stdout with `[build] …`, and the guard's JSON.parse threw. That broke every
`npm pack` (packaged-install-smoke on ubuntu+windows) and failed the guard's own
real-repo unit test (the only coverage-job failure). Force script-skipping via
the reliable `npm_config_ignore_scripts` env config (also removes the prepack
re-entry/recursion risk) and parse defensively from the JSON-array start.

* fix(publish): make the coverage guard deterministic — read `files`, not `npm pack`

The npm-pack-based guard timed out in CI: `npm pack`'s prepare/prepack lifecycle
is not skipped by `--ignore-scripts` (flag or env config) on the CI npm, so the
inner pack ran the full build (~20s+) — fine for the slow smoke job, but it blew
past vitest's 30s test timeout in the coverage job (and risked re-entering this
prepack guard).

Replace it with a deterministic, fast (~0.1s) check that needs no subprocess:
since `files: ["vendor"]` OVERRIDES `.npmignore` for the vendored subtree (so
`.npmignore` can never drop vendored source — verified), the ONLY lever that can
exclude source is narrowing the package.json `files` field. The guard now reads
`files` directly: a grammar "ships source" iff `files` includes the vendor
subtree AND the grammar carries a buildable source set on disk. A lean publish
that narrows `files` while a grammar lacks 6/6 prebuilds still fails the gate.

* feat(ci): vendored tree-sitter grammar update monitor

Adds a weekly (+ dispatchable) workflow that checks each vendored grammar against
its source-of-origin (npm for swift/kotlin, the GitHub default branch for
dart/proto; c is excluded — held at 0.21.4 for ABI safety) and opens a PR
re-vendoring any update that is ABI-COMPATIBLE with the pinned tree-sitter@0.21.1
(LANGUAGE_VERSION 13-14).

ABI awareness is the point: most upstreams have moved to ABI 15 (newer
tree-sitter), so a blind "bump to latest" would open PRs that can't build. The
monitor fetches the candidate source, reads its parser.c LANGUAGE_VERSION, and
only re-vendors 13/14 — incompatible updates are reported (notice + job summary),
never applied. (Confirmed live: dart/proto upstreams are ABI 15 today and are
correctly held; swift/kotlin are current.)

The re-vendor refreshes only the source-build inputs + runtime entrypoints,
preserving the GitNexus-hardened binding.gyp / README / prebuilds; the version
bump then triggers build-tree-sitter-prebuilds.yml, whose ABI-validation is the
final safety net so a subtly-wrong re-vendor can't silently ship. PR creation is
gated on the RELEASE_APP secret (skips with a notice if absent), mirroring the
build aggregate. Unit test locks the ABI gate; the script is import-safe.

* feat(ci): monitor tree-sitter-c too (report-only, ABI-pinned)

c was excluded from the update monitor, so an upstream c update went unnoticed.
Include it, but as report-only via a `hold`: c is ABI-pinned at 0.21.4
(#1242/#858) and must not auto-bump without a tree-sitter runtime upgrade, so an
available c update is detected + surfaced (notice + job summary) but never
auto-PR'd — even if it were ABI-13/14. `--apply c` refuses defensively. (Live:
upstream c is 0.24.1 / ABI 15 today, so c is doubly held — reported, not applied.)

* fix(ci): drop the shell in the grammar monitor's github fetch (CodeQL)

CodeQL flagged the GitHub-tarball fetch — it used `bash -c "gh api …/tarball/$ref
> src.tgz && tar xzf src.tgz"`, interpolating the API-derived ref into a shell
command (the shell-command-injection family: "this shell command depends on an
uncontrolled file name"). Replace it with a shell-free path: capture `gh api`'s
binary tarball as a Buffer via execFileSync, write it to a fixed file, and
extract with execFileSync('tar', …). No shell, no injection surface. Verified the
dart/proto fetch + ABI read still work.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:16:24 +01:00
Gergő Magyar
938111ad45
fix(ci): stabilize gitleaks after #2024 (#2027)
* fix(ci): stabilize gitleaks after #2024 and clear history false positive

Fetch PR base/head SHAs before gitleaks-action so fork PRs do not fail with
ambiguous revision ranges. Add .gitleaks.toml allowlist for fake keys in
http-embedder tests, rename the redaction probe key, and point the README CI
badge at abhigyanpatwari/GitNexus.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): restore gitleaks default rules and narrow allowlist

Add [extend] useDefault = true so default secret rules run again. Replace
file-level allowlist with regexes for known fake embedding API keys.
Route PR SHAs through env vars in the gitleaks fetch step.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update README.md

* Update README.md

* Update README.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:28:59 +01:00
Gergő Magyar
083aedbc41
refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023)
* refactor(ingestion): delete legacy call-resolution DAG + heritage processor (#942)

RING4-1: all 16 production languages (incl. Vue #940) are registry-primary, so
the legacy resolution legs only ran under the now-removed CI parity gate. Calls
and inheritance now resolve exclusively through scope-resolution
(Registry.lookup, preEmitInheritanceEdges, emitHeritageEdges, buildMro →
MethodDispatchIndex).

Removed:
- Call-resolution DAG: call-processor.ts legacy body (processCalls,
  processCallsFromExtracted, resolveCallTarget + all resolver/dispatch/chain
  helpers), model/resolve.ts MRO-via-HeritageMap, model/heritage-map.ts,
  type-env DAG types; inferImplicitReceiver/selectDispatch LanguageProvider
  hooks + Ruby impls; DispatchDecision/ImplicitReceiverOverride/ReceiverEnriched.
- Legacy heritage path: heritage-processor.ts, heritage-types.ts,
  heritage-extractors/, @heritage.* tree-sitter queries, heritageExtractor/
  heritageDefaultEdge/interfaceNamePattern wiring, worker + parse-impl heritage
  passes (parse-worker/parsing-processor lockstep), cross-file-impl DAG pass.
- Scope-parity infrastructure entirely (no legacy↔registry parity left to run):
  scripts/run-parity.ts, scripts/ci-list-migrated-languages.ts,
  ci-scope-parity.yml, test:parity, and the scope-parity ci.yml gate. Resolver
  integration tests still run via the normal tests job.

Kept (shared infra, NOT call-DAG-only): type-env.ts buildTypeEnv (field
extraction / structure phase / embeddings), model/resolve.ts c3Linearize +
gatherAncestors (mro-processor mroPhase), route/fetch/exported-type-map helpers
in call-processor.ts, preEmitInheritanceEdges (legacy-edge dedup simplified).

Acceptance: grep for resolveCallTarget/inferImplicitReceiver/selectDispatch/
buildHeritageMap/HeritageMap/processHeritage/heritageExtractor/@heritage. is zero
across src + test. tsc clean (both packages); resolver integration suite green
(bit-compatible EXTENDS/IMPLEMENTS/CALLS); scope-capture fingerprints unchanged
(python re-baselined: removed redundant ignored captures). ARCHITECTURE.md
updated to scope-resolution-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): apply autofix feedback (#942)

ce-code-review autofix pass on the RING4-1 deletion:
- parse-cache.ts: bump SCHEMA_BUMP 2→3 — ParseWorkerResult lost its `heritage`
  field, so stale on-disk caches must invalidate (prevents a rollback replaying
  a heritage-less cache into legacy code) [api-contract P2].
- parse-impl.ts: drop 3 now-unused type imports (ExtractedCall,
  ExtractedAssignment, FileConstructorBindings) left by the deferred-block
  removal — would fail the eslint CI gate [correctness+maintainability P1].
- AGENTS.md / CLAUDE.md / scope-resolver.ts contract doc: fix stale pointers to
  the deleted "§ Call-Resolution DAG" section + removed hooks; preserve the
  language-neutrality rule [project-standards P1].
- registry-primary-flag.ts / cross-file.ts / parse-impl.ts: refresh stale
  comments referencing deleted symbols (legacy DAG, runCrossFileBindingPropagation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ingestion): remove the vestigial isRegistryPrimary flag (#942)

With the legacy call-resolution DAG deleted, the per-language
`REGISTRY_PRIMARY_<LANG>` / `isRegistryPrimary` / `MIGRATED_LANGUAGES` flag had
only one meaningful state — every production language resolves via
scope-resolution — and an explicit `=0` override could only *disable*
resolution with no fallback (a footgun the review flagged). Removing it.

- Delete `registry-primary-flag.ts` and the now-dead `shadow-harness.ts`
  (legacy↔registry shadow-parity tool) + its test.
- Collapse the three flag gates to their behavior-preserving outcome
  (`SCOPE_RESOLVERS == MIGRATED_LANGUAGES`, so this is a no-op):
  - scope-resolution phase now runs for every registered `SCOPE_RESOLVERS`
    entry (was `∩ MIGRATED_LANGUAGES`).
  - import-processor `addImportGraphEdge` + parse-impl `shouldAccumulate`:
    the legacy emit/accumulate paths were already inert for migrated
    languages (scope-resolution owns IMPORTS via the imports-to-edges bridge);
    drop the flag term.
- Collapse flag-branching tests to the scope-resolution path and delete the
  csharp legacy-`=0`-leg describe blocks; remove the ruby/rust-scope env-forcing
  hooks (no-ops now).
- Refresh docs/comments (ARCHITECTURE.md "one registration", scope-resolver
  cookbook, phase deps) — adding a language is now a single `SCOPE_RESOLVERS`
  registration.

Verified: tsc clean (both packages); resolver integration tests green
(747 assertions across cobol/csharp/ruby/rust/typescript/go, IMPORTS edges
intact); grep for the flag symbols is zero across src + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(format): prettier formatting on #942 changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): drop legacy heritage-capture tests + re-baseline scope-capture fingerprints (#942)

Two CI failures from the #942 cleanup, surfaced by the tri-review + CI:

- tree-sitter-languages.test.ts: two tests asserted `@heritage.*` captures
  (Rust trait-impl, Dart extends/implements/with) that this PR removed. The
  acceptance grep used `@heritage\.` (with `@`); these reference the runtime
  capture name `heritage.trait` (no `@`), so they slipped the earlier sweep.
  Inheritance is now covered by the resolver integration suite. (fixed macos-latest)

- Re-baselined the scope-capture bench fingerprints for csharp/rust/ruby/java/
  javascript/kotlin (baselines.json) + python (python-scope/baseline-fingerprint.txt).
  The earlier test-cleanup reworded comments inside the lang-resolution fixture
  files (Shapes.cs, child.rs, derived.rb, IA.java/Plain.java, Service.js, F.kt,
  app.py) to scrub deleted-symbol references for the acceptance grep; those are
  the bench corpus, so capture node positions shifted. Capture LOGIC is
  unchanged — verified `--check` passes for all 14 langs + python. (fixed benchmarks)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs/chore: scrub remaining REGISTRY_PRIMARY + deleted-symbol references (#942)

Tri-review P3 follow-ups (verified):
- TESTING.md: rewrite the "Scope-resolution parity" section — the legacy
  dual-leg (REGISTRY_PRIMARY_<LANG>=0/1) and `npm run test:parity` no longer
  exist; resolver tests run once on the sole scope-resolution path in the
  normal tests job.
- scripts/bench-scope-resolution.ts: drop the inert `REGISTRY_PRIMARY_PYTHON=1`
  env set + usage hint (the flag is gone).
- ruby/scope-resolver.ts, php/captures.ts: re-point doc-comments off the
  deleted heritage-map.ts / heritage-processor.ts to the current behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): prettier format + regenerate scope-capture goldens (#942)

Two more CI failures, same root cause as the bench re-baseline (the
test-cleanup reworded comments in lang-resolution bench/golden-corpus fixtures):

- quality/format: prettier on tree-sitter-languages.test.ts (blank line left by
  the deleted heritage-capture tests) + TESTING.md (the rewritten section).
- tests/ubuntu/coverage: `csharp-captures-golden` (and python/ruby/rust) drifted
  because the edited fixtures feed the per-language capture-golden snapshots too
  (not just the bench). Regenerated via UPDATE_GOLDEN=1. Verified safe: only the
  edited-fixture entries changed; csharp `captureGroups` unchanged (38) — digest
  shifted from comment-position only; capture LOGIC untouched. 1168 scope-
  resolution tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(resolvers): drop createResolverParityIt wrapper, use vitest it directly

The parity-aware `it` wrapper became a no-op when #942 removed the legacy
call-resolution DAG (it just returned vitest's `it`). Remove it entirely so
the resolver tests call vitest's `it` directly instead of shadowing it with a
local `const it` (or `pit`/`rustParityIt`):

- helpers.ts: delete createResolverParityIt + its now-unused vitestIt import
  and VitestIt type.
- 16 files: drop `const it = createResolverParityIt('x')` and import `it`
  from vitest instead.
- ruby.test.ts (pit) + rust.test.ts (rustParityIt): rename calls to `it`.
- Scrub every comment that described the removed wrapper / dual-mode parity
  skip / legacy_skip gate (vue-scope, js/ts/dart/php/python headers, rust x2,
  cpp, swift x4, rust-coverage). Genuine test rationale is kept; only the
  vestigial two-leg framing is dropped. Accurate "legacy DAG (removed in
  #942)" historical notes are retained.

No fixtures touched (no bench/golden re-baseline). tsc clean; rust+ruby
resolver suites green (323 tests, incl. #1992 worker-path parity after a
local dist build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:07:37 +01:00
dependabot[bot]
ad36a86ab0
chore(deps): bump docker/login-action from 4.1.0 to 4.2.0 (#2020)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](4907a6ddec...650006c6eb)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:07 +01:00
dependabot[bot]
7982eaea12
chore(deps): bump docker/metadata-action from 6.0.0 to 6.1.0 (#2018)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](030e881283...80c7e94dd9)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:57 +01:00
dependabot[bot]
42ac44b838
chore(deps): bump docker/build-push-action from 7.1.0 to 7.2.0 (#2010)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](bcafcacb16...f9f3042f7e)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:44 +01:00
dependabot[bot]
617ca0d05e
chore(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#2019)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](4d04d5d948...d7f5e7f509)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:31 +01:00
dependabot[bot]
81b46518b2
chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 (#2017)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](9e0d7b8d25...7211b7c807)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:20 +01:00
Gergő Magyar
7ab6bd36d4
ci(devcontainer): harden smoke build against Docker Hub flakes (#1969)
* ci(devcontainer): retry Docker Hub syntax frontend and build

The devcontainers CLI injects `# syntax=docker/dockerfile:1`, which BuildKit
fetches from Docker Hub. Transient Hub timeouts caused main smoke failures
(run 26797815133). Pre-pull the frontend with backoff and retry the build
once, matching docker-build-push-retry policy.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci(devcontainer): address tri-review follow-ups on smoke retries

Make syntax-frontend pre-pull best-effort (continue-on-error) so build
retry still runs when Hub flakes only on pull. Clarify comment vs
docker-build-push-retry, and emit a notice when build retry succeeds.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:48:21 +01:00
Gergő Magyar
c4f82e4987
feat(devcontainer): add devcontainer for Claude/Codex/Cursor CLIs (#1875)
* chore: extend .gitattributes for shell scripts and binary assets

Append explicit `*.sh text eol=lf` and `*.bash text eol=lf` rules so
shell scripts (notably anything COPYed into a Linux container) check out
with LF endings on Windows hosts with `core.autocrlf=true`, regardless
of the auto-detection on the existing `* text=auto eol=lf` line. Add
binary markers for `*.node`, `*.wasm`, `*.onnx`, `*.so`, `*.dll`,
`*.dylib` so native and ML model artifacts aren't ever subjected to text
normalization.

The existing `* text=auto eol=lf` and `.husky/* text eol=lf` rules are
preserved. `git ls-files --eol` confirmed zero CRLF or mixed blobs in
the index, so no `--renormalize` was needed.

* feat(devcontainer): add cross-platform devcontainer for Claude Code, Codex, and Cursor CLIs

Add a Dev Container that pre-installs Claude Code (2.1.153, via Anthropic's
official Feature), OpenAI Codex CLI (pinned 0.134.0), and Cursor CLI alongside
the GitNexus native build chain. Opens via VS Code's Dev Containers extension
on Windows 11 (Docker Desktop + WSL2), macOS, or Linux without OS-specific
branches in devcontainer.json.

Topology and base
- Base image `mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm`
  (multi-arch, monthly patched, ships the `node` non-root user, zsh, `gh`).
- Node 22 LTS satisfies `gitnexus/`'s engines `>=22.0.0` and matches the
  `node:22-bookworm-slim` SHA-pinned base used by `Dockerfile.cli`.
- Single container with all three CLIs co-installed (vs. docker-compose
  per-tool) — prevailing 2026 community pattern, lowest daily-driver friction.

Persistence and auth
- Per-devcontainer named volumes scoped by `${devcontainerId}` for
  `/home/node/.claude`, `/home/node/.codex`, `/home/node/.cursor`,
  `/commandhistory`, and `/home/node/.npm`. Authentication survives rebuilds
  without leaking between workspaces.
- Four sub-workspace `node_modules` volumes (root, gitnexus, gitnexus-web,
  gitnexus-shared) keep tree-sitter native bindings and onnxruntime off the
  bind mount — the actual Win/Mac perf win.
- Credential mount paths are pre-created in the Dockerfile with
  `chown node:node` BEFORE `USER node`, so empty named volumes inherit
  correct ownership on first mount and first-run logins don't EACCES.
- `CURSOR_API_KEY` is injected via `containerEnv: ${localEnv:CURSOR_API_KEY}`
  (Cursor's documented headless path); falls back to interactive
  `cursor-agent login` when the host env var is unset.

Build-arg promotion
- Build args (`CLAUDE_CODE_VERSION`, `CODEX_VERSION`, `CURSOR_VERSION`, `TZ`)
  are promoted to ENV in the Dockerfile so lifecycle commands and shells can
  resolve them. Without this promotion, Docker ARG values are build-only and
  silently no-op at lifecycle time.

Workspace setup
- `postCreateCommand` chowns the four workspace `node_modules` volumes
  (Docker creates them root-owned), then installs in dependency order:
  root → gitnexus-shared (install + build) → gitnexus → gitnexus-web. The
  shared package must build before its consumers (`file:../gitnexus-shared`).

Ports
- 5173 (Vite dev) and 4173 (Vite preview) auto-forwarded.
- 4747 (`gitnexus serve`) marked `requireLocalPort: true` because
  `gitnexus-web/src/services/backend-client.ts` hardcodes
  `http://localhost:4747` as the default backend URL; a remapped port would
  silently break the web UI.

VS Code integration
- Recommended extensions: `anthropic.claude-code`,
  `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `eamodio.gitlens`.
- Settings: format-on-save with Prettier, ESLint auto-fix on save, zsh as
  default terminal profile, persistent zsh history via `HISTFILE` →
  `/commandhistory`.

Documentation
- `.devcontainer/README.md` covers WSL2 setup (clone inside WSL2 for IO and
  file-watcher reliability), first-time auth flows for each CLI, port-
  forwarding notes, LadybugDB container limitations, and the bumping
  procedure for each CLI version.
- `CONTRIBUTING.md` gets a "Containerized development (optional)"
  subsection pointing at the devcontainer README.

Deferred to a follow-up PR
- Opt-in egress firewall (originally planned as a fourth implementation
  unit). The Dev Containers spec makes `runArgs` static — toggling
  `NET_ADMIN`/`NET_RAW` capabilities cleanly requires either a separate
  `devcontainer-firewall.json` profile or an `initializeCommand`-generated
  overlay. Keeping this PR focused on the working baseline.
- Codespaces-specific tuning (works incidentally when the firewall is off,
  not actively tested).
- Inside-container Playwright e2e (needs Chromium libs not in the base
  image).

Verification deferred to user
- This change introduces a new dev tooling artifact. Validate by running
  `docker build .devcontainer/`, opening the repo in VS Code via
  "Dev Containers: Reopen in Container", confirming `claude --version`,
  `codex --version`, `cursor-agent --version` resolve inside the container,
  and `cd gitnexus && npm run test:unit` runs clean against the
  named-volume `node_modules`.

* fix(devcontainer): make interactive login the default auth path for all CLIs

The previous `containerEnv` injected `CURSOR_API_KEY: "${localEnv:CURSOR_API_KEY}"`.
When the host had no `CURSOR_API_KEY` set, this resolved to an empty
string and Docker injected `CURSOR_API_KEY=""` into the container.
Cursor CLI treats a set-but-empty `CURSOR_API_KEY` as "use this key"
rather than "fall back to stored login", which silently broke
`cursor-agent login` on the most common path — users who hadn't
explicitly opted into API key auth.

Drop `CURSOR_API_KEY` from `containerEnv`. Login is now the
unconditional default for all three CLIs (Claude Code, Codex CLI,
Cursor CLI); the named-volume + Dockerfile-chown pattern keeps
credentials persistent across container rebuilds for every login path.

Reorganize the README's auth section to put login first for all three
CLIs uniformly (matching the new behavior) and move API key
authentication into a separate "Alternative" section for CI/headless
use. Document that API keys are intentionally not auto-propagated from
the host and explain the export-in-shell or VS Code dotfiles-repo paths
for users who want them. Update the troubleshooting row to reflect the
new design.

* fix(devcontainer): install gitnexus-web before gitnexus in postCreateCommand

The previous order (root → gitnexus-shared → gitnexus → gitnexus-web)
broke at the `gitnexus` install step because `gitnexus`'s `prepare`
script runs `scripts/build.js`, which compiles `gitnexus-web` whenever
its source tree exists. In the devcontainer the entire workspace is
bind-mounted, so `gitnexus-web/` is present from the start — but its
`node_modules/` wasn't yet, so `tsc -b` failed with:

  error TS2688: Cannot find type definition file for 'vite/client'
  error TS2688: Cannot find type definition file for 'node'

Reorder so `gitnexus-web` installs before `gitnexus`. Verified
end-to-end via `npx @devcontainers/cli up`: container builds clean,
all three CLIs (Claude 2.1.153, Codex 0.134.0, Cursor) respond, and
`npx tsc --noEmit` inside `/workspace/gitnexus` passes.

Production Dockerfiles (`Dockerfile.cli` etc.) don't hit this because
they only COPY `gitnexus/` + `gitnexus-shared/`, so `gitnexus-web/`
doesn't exist at install time and `scripts/build.js` skips the web
step. The devcontainer's full-tree bind mount changes that calculus.

* fix(devcontainer): clear stale .husky/_ before npm install

When `npm install` runs the root `prepare` script (husky), husky tries
to copyfile `node_modules/husky/husky` → `.husky/_/h`. On Docker Desktop
Windows bind mounts, if `.husky/_/` already exists from a prior
container run, the new container's `node` user can't overwrite it via
the bind mount's permission translation and the install fails with:

  Error: EPERM: operation not permitted, copyfile
    '/workspace/node_modules/husky/husky' -> '.husky/_/h'

Drop `.husky/_` defensively in `postCreateCommand` before `npm install`
so husky always starts from a clean slate. `.husky/_` is a husky
runtime cache (gitignored), so removing it has no effect on the repo —
husky regenerates it. No-op for WSL2-side checkouts (where this class
of bind-mount permission collision doesn't occur).

Add a troubleshooting row to `.devcontainer/README.md` covering the
manual recovery (`rm -rf .husky/_` on the host) and the long-term fix
(clone in WSL2 — Windows-side bind mounts will keep biting on this
kind of issue across rebuilds with different UID alignment).

* feat(devcontainer): bind-mount host CLI config dirs for plugin/skill/memory sync

Switch the credential/config mounts from per-devcontainer named volumes
to bind mounts of `${localEnv:HOME}/.claude`, `~/.codex`, and
`~/.cursor`. Effect inside the container:

- Authentication is shared with the host. If you've already run
  `claude login` / `codex login --device-auth` / `cursor-agent login`
  on the host, you're already authenticated in the container.
- Plugins, skills, agents, memory, and settings sync both ways. Install
  a plugin in the container, it shows up on the host; add a custom
  agent on the host, the container sees it immediately.
- All devcontainers on the host share the same CLI state, mirroring
  how host shells already share it. (Per-workspace isolation of plugins
  was never a stated requirement; the previous per-devcontainer named
  volumes leaked nothing useful.)

Add `.devcontainer/ensure-host-config-dirs.cjs` and wire it as
`initializeCommand`. It runs on the host before container create and
guarantees `~/.claude`, `~/.codex`, `~/.cursor` exist, so Docker doesn't
reject the bind mount when a CLI has never been used on this host.
Cross-platform via Node `os.homedir()` + `fs.mkdirSync({recursive: true})`;
idempotent; no third-party deps.

Update `.devcontainer/README.md`:
- New "How CLI state is shared with your host" section explaining the
  bind-mount model up front so users know their host plugins/skills/
  memory carry into the container.
- Mark first-time-login section as skippable when the user is already
  authenticated on the host.
- Note the high-trust escape hatch: replace the three bind mounts with
  `type=volume` named volumes if the host/container trust boundary
  needs to be separated (Anthropic's reference pattern for enterprise).
- Replace the obsolete "rm named volume" troubleshooting row with one
  that covers EACCES/EPERM on the host-bind-mount path.

* refactor(devcontainer): address ce-code-review findings (P0 + 4 × P1 + 8 × P2 + 2 × P3)

Walkthrough resolution of the 16-finding ce-code-review on PR #1875. 15 of
16 findings applied; one (F12, Anthropic Feature floating tag) was
superseded by F6's Feature removal.

P0
- F1: WSL2 is now REQUIRED for Windows hosts, not just recommended.
  ${localEnv:HOME} resolves to empty string on Windows-native (no HOME env
  var) — bind mounts then point at /.claude, /.codex etc. and silently
  break. ensure-host-config-dirs.cjs wrote to USERPROFILE-derived paths
  via os.homedir(), so the two surfaces disagreed about which env var was
  "home" on Windows. README header reframed; "Windows 11 — WSL2 is required"
  section explains the mismatch concretely.

P1
- F2: Workspace `node_modules` volume names now include `-${devcontainerId}`
  so two GitNexus checkouts on the same host (~/work/GitNexus and
  ~/projects/GitNexus) don't share volumes and corrupt each other's
  installs.
- F3 + F5: `postCreateCommand` extracted to `.devcontainer/post-create.sh`
  with `set -euo pipefail` and six labeled echo steps so failure logs
  name the step instead of an opaque &&-chain index. Chown step extended
  to cover /home/node/.npm, /commandhistory, and /home/node/.local — these
  named-volume mount points were owned by build-time UID 1000 but the
  container's `node` is re-IDed at runtime by updateRemoteUserUID on
  non-1000 Linux hosts, leaving them unwritable until now.
- F4: Cursor installer downloaded to a temp file with curl --retry +
  --max-time; sha256 logged to build output before execution so drift
  across rebuilds is visible in CI logs. Full hard-pin (to a versioned
  downloads.cursor.com tarball with verified sha256) tracked as a
  follow-up in README "What's not included".

P2
- F6: Anthropic Feature replaced with a direct
  `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` so
  CLAUDE_CODE_VERSION actually pins the installed binary (the Feature
  ignored the ARG and pulled latest at install time). Honors the
  earlier "pin known-good versions" decision and resolves F12's
  floating-tag concern for this Feature.
- F7: Dockerfile ARG defaults dropped for the three version vars;
  `devcontainer.json` `build.args` is now the single source of truth.
  Standalone `docker build .devcontainer/` must pass --build-arg.
- F8: ensure-host-config-dirs.cjs deleted; `initializeCommand` now uses
  POSIX `mkdir -p` + `touch ~/.gitconfig` directly, dropping the
  host-Node-on-PATH prerequisite that broke on fresh Windows+Docker
  Desktop installs without Node.
- F9: ~/.gitconfig bind-mounted read-only so `git commit` inside the
  container uses the host's user.name / user.email. Read-only so
  container-side `git config --global` doesn't leak to host.
- F10: ~/.config/gh bind-mounted (read-write) so `gh pr create` /
  `gh pr checks` / `gh issue create` work inside the container without
  re-auth. AGENTS.md's commit + PR workflow now fully functional for
  agents inside the container.
- F11: CLAUDE_CONFIG_DIR removed from Dockerfile ENV; canonical value
  lives only in devcontainer.json containerEnv. Eliminates the two-file
  edit risk.
- F13: Mounts comment now documents per-instance vs per-workspace-name
  scoping rationale so future contributors don't guess.
- F14: README "Trust boundary, concretely" paragraph names the exfil
  path explicitly (malicious npm postinstall → OAuth tokens →
  ~/.claude/projects/<workspace>/memory/MEMORY.md secrets) and lists
  vendor-side rotation runbook entries.

P3
- F15: Dockerfile pre-create + chown of /home/node/.claude, .codex,
  .cursor dropped — those paths are bind-mounted, which fully shadows
  any image-side ownership. Only .npm, .local, /commandhistory still
  benefit from the pre-create.
- F16: README "Bumping CLI versions" section rewritten against the
  post-F6 reality: CLAUDE_CODE_VERSION and CODEX_VERSION are real
  pins; CURSOR_VERSION is informational only.

Verified locally: `docker build .devcontainer/ --build-arg ...` succeeds.
Smoke-tested image: `claude --version` (2.1.153), `codex --version`
(0.134.0), `cursor-agent --version` all resolve as the non-root `node`
user; named-volume mount points (/home/node/.npm, /commandhistory) are
node-owned at build time so non-1000 host UIDs get the post-create.sh
chown fix instead of EACCES.

* fix(devcontainer): cross-platform initializeCommand + soften Windows-native posture

The previous commit's `initializeCommand` was POSIX-only (`mkdir -p $HOME/...`).
VS Code on Windows runs the host shell as `cmd.exe /c ...`, which can't
parse POSIX syntax — `$HOME` doesn't expand, `mkdir -p` errors, the init
fails with `The syntax of the command is incorrect`, and container
creation aborts before Docker is invoked.

Switch `initializeCommand` to the spec's OS-keyed object form:
- linux/darwin (covers WSL2 because VS Code runs initializeCommand in
  the WSL shell when attached via the WSL extension): POSIX mkdir+touch,
  as before
- win32: PowerShell snippet that creates the same directories under
  $USERPROFILE and touches the gitconfig if missing

Soften the README's hard "WSL2 required" framing from the previous
commit. Reality per `@devcontainers/cli read-configuration` output:
`${localEnv:HOME}` on Windows-native resolves to `C:\Users\<name>`
(VS Code falls back to USERPROFILE), so the bind mount sources are
valid Windows paths and Docker Desktop handles the translation. The
earlier `accessing specified distro mount service` failure was a
separate Docker Desktop WSL-integration issue, not a HOME-resolution
issue. Windows-native works; it's just slower with more bind-mount
permission edge cases (the husky/_/h EPERM class). The README now
explains the tradeoff and steers toward WSL2 for performance + file
watchers + permission reliability, rather than blocking Windows-native
checkouts outright.

Update the troubleshooting row to reflect the new posture.

* fix(devcontainer): Node-based initializeCommand; bind-mount .ssh + .config/git

Two fixes bundled:

1. The previous commit's OS-keyed `initializeCommand` object was based
   on a misread of the Dev Containers spec. The object form on command
   properties is **named parallel tasks**, not OS dispatch — VS Code ran
   all three keys in parallel via cmd.exe on Windows, the POSIX branches
   failed, and container creation aborted before Docker was invoked.

   Restore the single-string Node-based form:
   `node .devcontainer/ensure-host-config-dirs.cjs`. Node works
   identically in cmd.exe on Windows and bash/zsh on Linux/macOS/WSL,
   and `os.homedir()` respects $HOME on POSIX and %USERPROFILE% on
   Windows. The script is idempotent (mkdirSync recursive is a no-op
   for existing dirs; touch is gated on .gitconfig existence).

   Document Node ≥18 on the host as the only host-side prerequisite
   beyond Docker Desktop and the VS Code Dev Containers extension.
   Anyone running Claude Code on the host already has it.

2. Extend the host-bind mount surface with `~/.ssh` and `~/.config/git`,
   both read-only:

   - `~/.ssh` lets commit signing + push over SSH remotes work inside
     the container without copying private keys. Read-only mount means
     container code can read keys but can't modify or delete them.
     (Threat: a malicious dep can still read private keys from inside
     the container; the read-only mount narrows write-side blast
     radius, not read-side. Documented in the trust-boundary section.)
   - `~/.config/git` covers XDG-style git config (`~/.config/git/config`,
     `~/.config/git/ignore`, `~/.config/git/attributes`) for users who
     keep settings there instead of `~/.gitconfig`. Read-only, same as
     `~/.gitconfig`.

   Update the CLI-state-sharing table and trust-boundary paragraph to
   reflect the expanded surface.

Re-adds .devcontainer/ensure-host-config-dirs.cjs (deleted before the
OS-keyed attempt).

* fix(devcontainer): fail-fast on Windows-native with HOME-not-set diagnostic

The previous commit's "Windows-native works" softening was wrong. VS Code
on Windows-native resolves `${localEnv:HOME}` by reading the host shell's
HOME env var, and cmd.exe has no HOME set — the bind sources collapse to
`/.claude`, `/.codex`, etc., and Docker errors:

  Error response from daemon: invalid mount config for type "bind":
    bind source path does not exist: /.claude

The @devcontainers/cli output that prompted the softening was misleading
because I ran it from a Bash session with HOME already set, not from VS
Code's cmd.exe call context. The original Finding-1 P0 — that Windows-
native silently breaks the bind-mount feature — was correct.

Three changes:

1. `ensure-host-config-dirs.cjs` detects the failure mode early:
   `if (process.platform === 'win32' && !process.env.HOME)` prints a
   targeted error message naming the root cause (cmd.exe has no HOME →
   ${localEnv:HOME} resolves empty → bind sources fail) and a step-by-step
   pointer to set up WSL2. Exits 1 so VS Code surfaces it as a clean
   container-creation failure, not the cryptic Docker bind-mount error.

2. README header reverted to "Windows 11 via WSL2" only (not "and
   Windows-native"). The "Windows 11 — WSL2 is required" section names
   the specific HOME-resolution mismatch concretely so future readers
   understand why the constraint exists.

3. Troubleshooting table gets a new row for the `ERROR: GitNexus
   devcontainer requires WSL2` message pointing at the setup section.

* feat(devcontainer): support Windows-native via auto setx HOME on first run

Reverses the "WSL2 required on Windows" posture. Windows-native now
works after a one-time auto-handled setup.

The root cause of the bind-mount failure: VS Code resolves
`${localEnv:HOME}` by reading its own process env, and Windows doesn't
set `HOME` by default — Windows uses `USERPROFILE`. So the bind sources
were collapsing to `/.claude`, `/.codex`, etc., and Docker rejected them.

`ensure-host-config-dirs.cjs` now handles this automatically on Windows
hosts where `HOME` is unset:

1. Runs `setx HOME "%USERPROFILE%"`, which writes to the user-level
   Windows environment (HKCU\Environment) — no admin required. Every
   future user process inherits HOME from there.
2. Prints a clear one-time setup banner explaining the user needs to
   fully restart VS Code (File > Exit, not just close the window) for
   VS Code to pick up the new env at its next startup.
3. Exits 1 so VS Code surfaces this as a clean container-create failure
   instead of letting Docker error opaquely later.

On the second Reopen-in-Container attempt, `HOME` is now set in VS
Code's env, the script skips the setup block, creates the bind-mount
source dirs, and the container builds normally. Subsequent rebuilds
have no extra steps.

Mac, Linux, and WSL2 hosts have `HOME` set by the shell, so the new
block is a no-op there. Same `devcontainer.json` works across all
supported hosts.

README rewritten to reflect the new posture:

- Header lists Windows 11 (native) as a supported host alongside macOS,
  Linux, and WSL2, with a note that Windows-native gets a one-time
  HOME setup handled by the initializeCommand.
- New "Windows 11 setup" section walks through the auto-handled setup
  flow + a manual `setx HOME "%USERPROFILE%"` fallback for users who
  want to do it themselves.
- "Known trade-offs of Windows-native vs WSL2" subsection lays out the
  Docker Desktop Windows bind-mount edge cases (file watchers, npm
  install perf, husky/_ EPERM) so users opting into Windows-native do
  so eyes-open. WSL2 remains documented as the faster path for users
  who want it, but it's no longer the only supported one.
- Troubleshooting table gets two new rows: the one-time setup banner
  (with "what to do" instructions) and the residual `bind source path
  does not exist` case (run setx manually + fully exit VS Code).

* fix(devcontainer): drop ~/.gitconfig bind mount; defer to VS Code auto-copy

VS Code's Dev Containers extension auto-copies the host's gitconfig into
the container at attach time using `(dd ...) >> /home/node/.gitconfig`.
A read-only bind mount of ~/.gitconfig blocks that write, so attach
failed with `cannot create /home/node/.gitconfig: Read-only file system`.
Making it read-write would let the append succeed, but the bind mount
means the host file and the container file are the same file — VS Code's
append would double the host gitconfig contents on every container
start.

Drop the ~/.gitconfig bind mount entirely. VS Code's auto-copy is the
purpose-built mechanism for this, gives the container the host's
user.name / user.email transparently, and avoids both the read-only
write failure and the append-duplication trap. The container ends up
with a writable /home/node/.gitconfig that's a copy of the host's, not
a mount.

The remaining six bind mounts (.claude, .codex, .cursor, .ssh, .config/git,
.config/gh) keep their existing modes — XDG-style git config under
~/.config/git is unaffected by VS Code's auto-copy (which only targets
~/.gitconfig), so its read-only bind mount stays.

Also remove the `.gitconfig` touch from ensure-host-config-dirs.cjs
(now unnecessary) and update the README CLI-state table, sharing
explanation, and troubleshooting row to reflect that gitconfig flows
in via VS Code auto-copy rather than the bind mount.

* feat(devcontainer): bind-mount ~/.docker, ~/.aws, ~/.azure for agent workflows

Extend the host bind-mount surface so coding agents inside the container
inherit cloud + container-registry auth from the host without any
per-container setup:

- ~/.docker (read-write) — Docker registry auth (config.json) + buildx
  config. Container-registry pushes (ghcr.io, docker.io) from inside the
  container pick up host `docker login` state. Read-write because the
  Docker CLI refreshes credential-helper tokens.
- ~/.aws (read-only) — AWS CLI / SDK credentials. Read-only because
  rotating creds typically happens via the host. Empty on this dev box,
  so forward-compatible: the moment you `aws configure` on the host the
  container picks it up on the next rebuild.
- ~/.azure (read-only) — Azure CLI credentials. Same pattern as ~/.aws.

`ensure-host-config-dirs.cjs` extends to mkdir these three on init so
the bind mounts always have a valid source even if a CLI has never been
used on this host.

The Docker CLI itself isn't installed in the container by default — the
~/.docker/ mount is inert until you add `docker-outside-of-docker:1` or
similar Feature. README now calls this out under "What you still don't
have inside the container" so it's obvious which CLIs are agent-ready
and which need a feature add to become useful.

README updates:

- Bind-mount table gains a "Why" column and rows for the three new
  mounts, making it clear at a glance what each one enables.
- Trust-boundary section lists Docker registry tokens, AWS, and Azure
  creds in the read-side exfil path so the threat model stays honest as
  the credential surface grows.
- New subsection lists not-included CLIs (Docker, AWS, Azure, gcloud,
  kubectl, private-npm) with the exact Feature ID or mount snippet
  needed to enable each — turns "I want my agent to do X" into a
  one-line config change.

Verified locally: `npx @devcontainers/cli read-configuration` resolves
all 9 host bind mounts to valid C:\Users\<name>/* paths on Windows.

* refactor(devcontainer): hybrid AI CLI config — read-only host share + per-container credentials

Restructure the Claude Code / Codex / Cursor mount topology to fix the
silent first-run-UI bug surfaced in PR testing, and to harden against
the host-write-through escape class the previous bind-mount design
exposed.

The actual root cause of the first-run wizard firing on the user's
screenshot — confirmed via three parallel research agents (best
practices, framework docs deep dive of the OpenAI Codex Rust source,
adversarial design review) — was NOT a credential permission check.
Claude Code splits state across `~/.claude/.credentials.json` AND
`~/.claude.json` (a FILE at $HOME, sibling of the `.claude/` dir).
The latter holds `hasCompletedOnboarding`, `userID`, `oauthAccount`
metadata, MCP user-scope config, and per-project trust state — and
Claude Code reads it at literal `$HOME/.claude.json`, not via
`CLAUDE_CONFIG_DIR`. The previous design mounted `~/.claude/` but
left `~/.claude.json` outside the topology entirely, so every container
started with a missing onboarding-state file and re-ran the wizard.

Confirmed by tfvchow/field-notes-public#10:
"Persisting .credentials.json alone is NOT sufficient. Without
.claude.json, Claude Code treats the session as a fresh install and
prompts for login regardless of valid credentials being present."

The new topology:

**Mounts**
- `${localEnv:HOME}/.claude` → `/host/.claude` (read-only bind)
- `${localEnv:HOME}/.codex` → `/host/.codex` (read-only bind)
- `${localEnv:HOME}/.cursor` → `/host/.cursor` (read-only bind)
- `${localEnv:HOME}/.claude.json` → `/host/.claude.json` (read-only bind)
- `claude-config-${devcontainerId}` → `/home/node/.claude` (named volume)
- `codex-config-${devcontainerId}` → `/home/node/.codex` (named volume)
- `cursor-config-${devcontainerId}` → `/home/node/.cursor` (named volume)

**containerEnv** gains `CODEX_HOME=/home/node/.codex` (Codex's own env
override, per its public Rust source). `CLAUDE_CONFIG_DIR=/home/node/
.claude` was already set.

**`post-create.sh`** stages the named volumes on first run:

- Symlinks shareable subdirs from `/host/.claude` into the named volume:
  `plugins/`, `skills/`, `agents/`, `memory/`, `commands/`. Codex gets
  `config.toml` symlinked. Cursor has no shareable subdirs (cli-config
  .json conflates auth and settings).
- Copies `.credentials.json`, `auth.json`, `cli-config.json` on first
  run with `chmod 600`. After first run, container manages its own
  refresh; host's credentials untouched.
- Copies `~/.claude.json` on first run (with stub
  `{"hasCompletedOnboarding":true,"installMethod":"global"}` fallback
  for hosts that haven't run Claude Code). This is the fix for the
  observed onboarding-wizard loop.

`ensure-host-config-dirs.cjs` now also touches `~/.claude.json` on the
host if missing, so the bind mount has a valid source on hosts that
have never run Claude Code.

**Why read-only + named volume vs. the previous full bidirectional
bind mount:**

1. **Host filesystem write-through escape, eliminated.** Previous
   design symlinked `plugins/`, `agents/`, `skills/` write-through
   into the host's `~/.claude/` — a malicious npm package in the
   workspace dep tree could drop `agents/evil.md` into the host's
   config, which the next host Claude session would auto-load. The
   read-only `/host` mount blocks this; container compromise no
   longer persists across teardown via host-side autoload.
2. **Windows bind-mount perm-flattening, sidestepped.** Files
   surfaced through a Docker Desktop Windows bind mount appear as
   `root:root` mode `777`. Credentials in the named volume come with
   proper Linux ownership and `chmod 600` — what each CLI expects on
   write (none enforces on read, but write-side hygiene matters for
   the host's understanding of "where credentials live").
3. **No `ide/` lock-file collisions.** Previous design symlinked
   `~/.claude/ide/` write-through, including per-PID lock files. Host
   PID and container PID namespaces are unrelated → lock-file PIDs
   misclassify dead processes as alive. Skipping `ide/` keeps lock
   files container-local.
4. **No `projects/` ghost dirs.** Host encodes the workspace path as
   `D--development-coding-GitNexus`, container as `-workspace`.
   Bidirectional `projects/` symlinks would split memory and session
   state across two ghost project dirs for what is conceptually the
   same project. Skipping `projects/` keeps per-project state
   container-local; host's projects/ stays untouched.
5. **No `settings.json` version drift.** Container is pinned to a
   specific Claude Code version (`CLAUDE_CODE_VERSION` build arg);
   host floats with auto-update. Bidirectional `settings.json` writes
   produced silent schema rollback. Skipping settings.json keeps each
   side authoritative for its own version.

**README** rewritten in the same section to describe the new topology
honestly: what's shared, what isn't, the OAuth refresh-token
divergence between host and container, per-CLI quirks (macOS Keychain
storage, Cursor's known upstream in-container auth bug, Codex
keyring storage). Trust-boundary section updated to name the threat
model accurately — same read surface as before (malicious dep can
still READ all credentials), but write-through into host plugin/agent
dirs is now blocked.

Verified locally: `@devcontainers/cli read-configuration` resolves all
19 mounts correctly on Windows, `post-create.sh` parses, and
`ensure-host-config-dirs.cjs` idempotently touches `~/.claude.json`.

Research backing this design:

- Anthropic Claude Code devcontainer docs (named-volume pattern):
  https://code.claude.com/docs/en/devcontainer
- tfvchow/field-notes-public#10 (both files required):
  https://github.com/tfvchow/field-notes-public/issues/10
- anthropics/claude-code#29029 (VS Code extension strips
  hasCompletedOnboarding):
  https://github.com/anthropics/claude-code/issues/29029
- OpenAI Codex Rust source (no read-side perm check):
  https://github.com/openai/codex/blob/main/codex-rs/login/src/auth/storage.rs
- Cursor CLI in-Docker auth issue:
  https://forum.cursor.com/t/cursor-agent-authentication-issue-inside-docker/143995

* fix(devcontainer): resync AI CLI state from host on every container-create

Two bugs were causing Claude Code to fire the onboarding wizard inside the
container even with valid host credentials:

1. Missing the second state file. Claude Code 2.1.x writes a small `.claude.json`
   INSIDE `CLAUDE_CONFIG_DIR` (carrying migration tracking + userID), not just the
   one at `$HOME/.claude.json`. If the userIDs in the two files disagree, Claude
   treats the session as inconsistent and re-onboards. The previous post-create.sh
   only copied the `$HOME` one.

2. First-run guards (`[ ! -e $dst ]`) skipped the copy when stale named volumes
   from earlier rebuilds still had the prior session's state in them, leaving the
   container desynced from the host.

Replace `copy_on_first_run` with `sync_from_host` that always overwrites from
host on container-create. `link_readonly_share` now clears stale non-symlink dst
entries before linking. Copies both `$HOME/.claude.json` and
`$CLAUDE_CONFIG_DIR/.claude.json` so userIDs stay aligned. Container can still
mutate its own state between rebuilds; resync only happens on rebuild
(postCreate boundary).

* docs(devcontainer): document sync-from-host design + dual-source auth flow

README still described the old "first-run copy" behavior. After the
post-create.sh change to always-sync-from-host, the design works either
direction:

- Log in on host → next container-create syncs the credentials into the
  named volume.
- Log in inside the container → the named volume persists the login across
  rebuilds; the host has no source to overwrite from, so it stays alone.

Also documents the two-Claude-state-files trap (`$HOME/.claude.json` AND
`$CLAUDE_CONFIG_DIR/.claude.json`, both with the same userID required), and
the volume-deletion recovery path for stale named volumes carried over
from earlier rebuilds.

* fix(devcontainer): full plugin/config parity by dropping CLAUDE_CONFIG_DIR + syncing settings.json

Two changes that together give the container the same plugins and configs
as the host for all three AI CLIs (login stays per-container):

1. Drop CLAUDE_CONFIG_DIR from containerEnv. The named-volume mount target
   `/home/node/.claude` already matches Claude's default `~/.claude`, so
   the env var added no behavior — but setting it changed which file
   Claude reads `hasCompletedOnboarding` from. With it set, Claude reads
   `$CLAUDE_CONFIG_DIR/.claude.json` (the small identity-only file that
   does NOT carry `hasCompletedOnboarding`); without it, Claude reads
   `$HOME/.claude.json` (the big onboarding-state file that does). The
   wizard fires every container-create when set, skips when unset.

2. Sync `settings.json` from host (Claude) + symlink `memories/` and
   `skills/` from host (Codex). Theme + `enabledPlugins` +
   `extraKnownMarketplaces` live in `settings.json` — without syncing
   it, the theme picker fires and host-installed plugins stay disabled
   even though their files are symlinked in. Codex's `memories/` and
   `skills/` are the symmetric Codex user-installed surface, now shared
   the same way Claude's plugins/skills/agents/memory/commands are.

Cursor stays as-is — `cli-config.json` conflates auth+settings (already
synced), and there's no separate plugin surface to mirror.

Login details remain per-container by design (acceptable to re-login on
rebuild). Everything else — plugins, skills, agents, memory, MCP user-
scope config, project trust, theme, plugin enablement — now matches
host on every container-create.

* refactor(devcontainer): hybrid RW bind + per-container creds — fixes EROFS on in-container plugin install

The previous Option B topology (RO host stage + named volume + symlinks
into the volume) made `/plugin marketplace add` inside the container fail
with EROFS — the symlinks pointed at a read-only mount, so Claude
couldn't create new marketplace dirs. Switch to a hybrid: shareable
content (plugins/skills/agents/memory/commands/settings.json/$HOME/.claude.json
for Claude; config.toml/memories/skills for Codex) gets a direct RW bind
from host so reads and writes go bidirectionally; credentials + the
small identity file stay in per-container named volumes so logout in
container doesn't log out host.

Mount precedence does the heavy lifting: the named volume mounts at
/home/node/.<cli> first, then sub-path bind mounts overlay specific
sub-paths. Container's view at /home/node/.claude/plugins/ is the host
dir; container's view at /home/node/.claude/.credentials.json is the
named volume's file.

What this gives you:
- /plugin marketplace add in container = installed on host
- New skill on host = visible in container immediately (no rebuild)
- claude logout in container = host stays logged in
- compound-engineering plugin enabled on host = enabled in container
- Theme picker fires once (or never if host has theme set)

What it costs:
- Write-through: a compromised npm dep in workspace deps can write to
  host ~/.claude/{plugins,skills,agents,memory,commands}/. Documented
  trade-off; for personal dev, accepted. Credentials still per-container.

post-create.sh becomes much simpler — only syncs the four credential
files from host into the named volumes. No more symlink dance, no more
state-file merging.

ensure-host-config-dirs.cjs gains the new bind sources: the shareable
subdirs and settings.json/config.toml files get mkdir/touched on host
so Docker doesn't reject the mount when a CLI has never been used.

* fix(devcontainer): translate host plugin registry paths to Linux on rebuild

The previous topology bind-mounted the entire `~/.claude/plugins/`
directory from host. That brought through plugins, marketplaces, and
extracted cache content correctly — but ALSO brought through the
registry JSONs (`known_marketplaces.json`, `installed_plugins.json`,
`plugin-catalog-cache.json`) which carry absolute OS-native paths:

  "installLocation": "C:\Users\gergo\.claude\plugins\marketplaces\X"
  "installPath": "C:\Users\gergo\.claude\plugins\cache\Y\Z"

Claude in the Linux container fails to resolve these Windows paths and
reports `Marketplace X failed to load: cache-miss`.

Split the topology:
- `plugins/marketplaces/` (git clones) and `plugins/cache/` (extracted
  plugin files) stay bidirectional RW binds — content is path-independent.
- Registry JSONs move into the per-container named volume. post-create.sh
  reads host's versions, rewrites any absolute path ending in
  `/.claude/plugins/<rest>` (Windows `C:\Users\...` and POSIX
  `/Users/...` / `/home/...` patterns) to `/home/node/.claude/plugins/<rest>`,
  and writes the translated result to the volume.

What this gets you:
- Plugin installed on host → next container rebuild has it (translated).
- Plugin installed inside container → lives in volume registry; lost on
  rebuild (consistent with credentials model). Re-install on host for
  persistence.

ensure-host-config-dirs.cjs now also creates `plugins/marketplaces/` and
`plugins/cache/` on host if absent (Docker rejects bind mounts whose
source doesn't exist).

* fix(devcontainer): clean stale plugin/skill symlinks from prior design before writes

A user upgrading from Option B (read-only host stage + symlinks) to the
current hybrid RW-bind topology hit EROFS in post-create.sh when the
plugin registry path-translator tried to write
`/home/node/.claude/plugins/known_marketplaces.json`. The named volume
still carried `/home/node/.claude/plugins -> /host/.claude/plugins`
(Option B's symlink). The new design's sub-path bind mounts at
`plugins/marketplaces` and `plugins/cache` overlay through the symlink,
but writes to the parent dir itself resolve via the symlink to the RO
host stage and fail.

Drop any leftover symlinks at known target paths early in step 2 so the
mkdir/writes that follow land in the volume.

* refactor(devcontainer): split workspace-deps to updateContentCommand

post-create.sh was doing two unrelated jobs: workspace dependency install
(four `npm install` runs in topological order) and AI CLI credential
sync. They have different lifecycle needs — deps should re-run when
lockfiles change, AI sync should run once per container — but both were
gated on container-create.

Per Dev Container spec lifecycle, `updateContentCommand` is the right
hook for workspace deps: runs at container-create AND on content
changes (lockfile updates). `postCreateCommand` is right for AI CLI
sync: container-create only.

Move steps 3-7 (husky cleanup + four `npm install` runs) into
install-deps.sh wired as `updateContentCommand`. Split the chown step
too — install-deps owns workspace-side dirs (node_modules volumes,
~/.npm), post-create owns AI-side dirs (~/.claude, ~/.codex, ~/.cursor,
/commandhistory, ~/.local). Each script now has one concern.

post-create.sh drops from ~187 lines to 148; install-deps.sh is 56 lines
new. Faster rebuilds when nothing about deps changed (the credential
sync + path translation work still runs every container-create, but the
npm install dance no longer does).

Research backing (no other simplification applies):
- Anthropic's reference devcontainer uses pure named volumes; no
  host-state inheritance pattern is published.
- Path translation has no upstream fix (issues #21916, #10379 closed
  without resolution). Our Node rewrite is the workaround.
- pnpm workspaces (`pnpm -r install`) would replace the four installs
  with one command, but that's a real refactor (touches
  gitnexus/scripts/build.js + 4 package.json files); deferred.
- `HUSKY=0` in containerEnv would drop the `rm -rf .husky/_` hack, but
  would also stop pre-commit hooks from firing inside the container;
  deferred.

* fix(devcontainer): drop single-file binds — fixes Codex `batchWrite failed in TUI`

On Docker Desktop Windows the named volumes are ext4 (`/dev/sdd`) while
single-file bind mounts from the Windows host land as 9p (drvfs).
Different filesystems → atomic config writes (write `foo.tmp`, then
rename onto `foo`) trip EXDEV `inter-device move failed` /
`Device or resource busy`.

Codex's TUI surfaces this as `config/batchWrite failed in TUI` when
saving model preference. Claude's writes to settings.json / .claude.json
fail the same way, silently.

Reproduction in container:
  $ echo x > /tmp/foo.toml; mv /tmp/foo.toml /home/node/.codex/config.toml
  mv: inter-device move failed: ... Device or resource busy

Fix: drop the three single-file bind mounts. Sync host's versions into
the named volume on container-create via `sync_from_host` (same pattern
already used for credentials). Atomic rename within the volume works
because everything is ext4.

Trade-off: container writes to these files no longer propagate to host;
they stay in the volume until next rebuild, which re-syncs from host.
Host is source of truth on rebuild — same model as credentials. Plugin/
skill/agent/memory/command DIRS still bind-mount bidirectionally (atomic
writes within a dir bind stay on one filesystem, no EXDEV).

Files affected:
- ~/.codex/config.toml
- ~/.claude/settings.json
- ~/.claude.json (HOME-level — added `/host/.claude.json` RO mount back
  for sync_from_host to read)

* chore(autofix): apply prettier + eslint fixes via /autofix command

* feat(devcontainer): Codex + Cursor plugin/config host parity with Claude

Codex plugins installed in the container never reached the Windows host
because, unlike Claude, the Codex plugin tree wasn't bind-mounted —
only memories/ and skills/ were. Verified via live /proc/mounts: Claude
binds 6 shareable dirs (incl. plugins/marketplaces + plugins/cache),
Codex bound 2. So `codex plugin add` wrote into the ext4 named volume
and stayed there.

Codex changes:
- Bind the WHOLE ~/.codex/plugins dir + ~/.codex/prompts (plus existing
  memories/skills). Strace of two real `codex plugin add` runs proved
  the installer stages INSIDE plugins/cache/<marketplace>/ and renames
  intra-dir, so a single 9p bind of plugins/ keeps the rename intra-fs —
  no EXDEV (the bug that broke single-file binds). .tmp/ stays on the
  volume (it's the cross-fs staging source). No path translation needed:
  Codex enablement lives in config.toml as git URLs, not FS paths.
- Verified live: `codex plugin add compound-engineering@...` now writes
  through to C:\Users\...\.codex\plugins\cache\ on the Windows host, and
  host-created files appear in the container (bidirectional).

Cursor changes (review found cursor-agent has a real plugin surface, not
editor-only — Cursor 2.5 Marketplace shared by IDE + CLI):
- Bind plugins/marketplaces, plugins/local, rules, commands, agents,
  skills (dir binds, EXDEV-safe).
- Copy-on-create mcp.json (single file → EXDEV-unsafe as bind), alongside
  the existing cli-config.json.
- Translate plugins/installed_plugins.json (carries absolute Windows
  paths like Claude's) — generalized the existing path-rewrite to run
  for both Claude and Cursor.
- hooks.json deliberately NOT shared (runs shell commands → supply-chain
  surface); documented as opt-in.

ensure-host-config-dirs.cjs pre-creates all new host bind sources.
post-create.sh defensive symlink cleanup extended to the new Codex/Cursor
paths. README updated with the accurate per-CLI share/sync/translate
matrix.

Design adversarially verified (straced installs, EXDEV primitive tests,
sqlite-under-bind check, path-encoding check) before implementing.

* fix(devcontainer): resolve ce-code-review findings (doc drift, chown scope, .cjs extraction, CI smoke)

Multi-agent review (9 reviewers) found the devcontainer files carried
comments + README from the abandoned read-only-symlink design, plus real
behavioral gaps. Resolved all actionable findings (no deferrals).

Documentation drift (the headline — stale comments described a security
model opposite to what shipped):
- README "Trust boundary" claimed a malicious dep "cannot write back …
  the read-only /host mount blocks the write." FALSE — the shareable dirs
  are RW-bound. Rewrote to document the bidirectional write-through, what
  stays one-way (credentials never flow back), and how to close it.
- devcontainer.json mount group-1 comment described "selectively symlinks
  … read-only eliminates write-through" — replaced with the RW-bind reality.
- Header "Windows-native is unsupported" -> supported (auto HOME setup).
- containerEnv comment "credentials persist in host-bind-mounted dirs" ->
  they live in the named volumes.
- hooks.json exclusion documented honestly as a partial mitigation, not a
  clean boundary (commands/agents/skills/rules are equally executing).
- ~/.local "named volume" -> image directory.

Behavioral fixes:
- chown -R recursed into the RW host binds (could rewrite host ownership /
  EPERM-abort provisioning on non-UID-aligned Linux). Switched to
  `find -xdev` per dir so chown stays on the volume filesystem.
- Cursor installer wrapped in `timeout 300` — its inner binary download
  isn't covered by curl --max-time and could hang docker build forever.
- Removed dead CURSOR_VERSION ARG/ENV/build-arg (never consumed; "latest"
  implied a pin the installer can't honor). Documented why Cursor is unpinned.

Extraction + tests (the two inline post-create.sh node heredocs were
unlintable and untestable; the path regex had had bugs):
- seed-claude-config.cjs — installMethod-strip seed, now with a non-object
  guard (a bare-value/array host .claude.json could otherwise slip the
  try/catch and silently re-trigger onboarding) and labeled write errors.
- translate-plugin-registries.cjs — plugin-registry path translation with
  labeled errors.
- translate-plugin-registries.test.cjs — 12 tests (Windows/POSIX paths,
  cross-CLI isolation, nested objects, non-object/empty-config guard).
- post-create.sh calls the modules via $SCRIPT_DIR.

CI:
- .github/workflows/ci-devcontainer.yml — runs the unit tests + shell
  syntax checks + a `@devcontainers/cli build` smoke on .devcontainer/**
  changes. Conforms to the repo concurrency convention (validator passes).

Documented (real gaps, fixes are honest docs since no correct auto-fix
exists): user-scope MCP servers with absolute host command paths don't
resolve in-container; user-scope config is copy-on-create so host edits
need a rebuild; in-container plugin installs get shadowed by an empty host
bind on rebuild (recovery noted); plugin installs are single-writer across
checkouts; gh/docker RW-vs-ssh/aws/azure-RO rationale.

Verified: fresh `@devcontainers/cli up` succeeds; installMethod stripped,
registry translated to Linux paths, credentials node:node, 12/12 tests pass.

* fix(devcontainer): set persist-credentials:false on CI checkouts + prettier

- zizmor `artipacked` (CodeQL/GitHub Advanced Security) flagged both
  actions/checkout steps in ci-devcontainer.yml: checkout defaults to
  persist-credentials:true, leaving GITHUB_TOKEN in .git/config where it
  can leak into uploaded artifacts. Both jobs are read-only (run tests /
  build smoke, never push), so persist-credentials:false is correct —
  matches the repo convention in codeql.yml / ci-tests.yml.
- Ran prettier 3.8.0 over the new .cjs modules + test (single-quote/style
  normalization to match the repo). JSON/YAML were already compliant;
  README is in .prettierignore; .sh has no prettier parser. Behavior
  unchanged — 12/12 transform unit tests still pass.

* fix(devcontainer): resolve adversarial review findings (pins, RO mounts, tests)

Resolves the blocking + actionable findings from the PR #1875 review:

- Pin base image by digest as bare name@digest [#1]. The :tag@digest form
  trips the @devcontainers/cli image-name parser (which builds this image
  in CI and in VS Code "Reopen in Container"); bare name@digest is the
  parser-compatible form. Verified by a full local build.
- Pin Cursor by version + per-arch sha256 and fetch the artifact directly
  instead of executing cursor.com/install; fail-closed on mismatch [#2].
- Mount ~/.config/gh and ~/.docker read-only so a compromised dep can't
  rewrite the host GitHub token / Docker credHelper [#4].
- Pin @devcontainers/cli@0.87.0 in the CI smoke [#5].
- chown via find -xdev in install-deps.sh (symlink-safe; matches
  post-create.sh) [#6].
- Add filesystem-I/O tests (translate/readHostConfig/seed main/ensurePaths)
  and refactor ensure-host-config-dirs to be unit-testable [#7].
- Stop pre-creating settings.json/config.toml on the host; only the real
  single-file bind source (.claude.json) is touched [#10].
- Add a prominent top-of-README security callout for the RW write-through
  trade-off and reframe the deferred egress firewall as the key missing
  compensating control [#3, #9].

Full devcontainer build verified locally (digest pull + pinned Cursor
download/extract/symlink). 24/24 config-transform tests pass.

* fix(devcontainer): resolve local adversarial-review findings (low/nit)

Follow-up to a local branch review (run after the cloud review crashed before
producing findings); all 5 confirmed findings were low/nit:

- chown via `find -xdev -exec chown -h`: add -h so chown acts on a symlink
  ITSELF, not its target. Without it a dangling node_modules/.bin link aborted
  provisioning under `set -e`, and a cross-fs symlink target could be
  dereferenced/rewritten. Verified in a clean container (regular files still
  chowned; dangling link no longer aborts; cross-fs target untouched). Applied
  to install-deps.sh and post-create.sh; the inline comments are corrected to
  describe -xdev (descent bound) and -h (no deref) as the two distinct guards.
- Reword the .cjs header claims from "lintable" to "unit-tested and
  prettier-checked": ESLint applies no rules to .cjs in this repo; CI only
  prettier-checks them.
- README: the initializeCommand is `node ensure-host-config-dirs.cjs`, which
  creates the full bind-source set, not a bash `mkdir -p` of four dirs.
- ci-devcontainer.yml: document that the x64 runner exercises only the amd64
  Cursor branch; the arm64 sha/URL is hash-pinned (verified against the
  published artifact) but not built in CI.
- Make the seed chmod-644 test meaningful: pre-create dst at 0o600 so only the
  explicit chmodSync can widen it (the prior assertion passed under the default
  umask regardless of whether the chmod ran).

25/25 config-transform tests pass; arm64 + x64 Cursor artifacts verified.

* docs(devcontainer): rewrite code comments in plain English

The devcontainer comments had grown dense and jargon-heavy. Rewrite them
across all 9 files into short, plain-English sentences — same facts and
reasoning, just clearer wording.

Comments only; no code changed. Verified: the diff touches comment lines
only, 25/25 config-transform tests pass, devcontainer.json is still valid
JSONC with build.args + readonly mounts unchanged, shell scripts pass
`bash -n`, and prettier is clean.

* feat(devcontainer): persist AI CLI session state across container recreation

Add dedicated per-workspace named volumes (mount group 6) for the three
AI CLIs' session/resume state so `claude --resume`, `codex resume`, and
`cursor-agent resume` survive a rebuild, a full delete-and-recreate, and
the `docker volume rm <cli>-config-*` re-login fix:

  - Claude -> ~/.claude/projects
  - Codex  -> ~/.codex/sessions
  - Cursor -> ~/.cursor/chats + ~/.cursor/projects

The volumes are SEPARATE from the credential/config volumes and keyed
like the node_modules volumes (${localWorkspaceFolderBasename}-...-
${devcontainerId}), so wiping a config volume to force a re-login no
longer destroys session history. Session state already survived a plain
rebuild (it lived in the config volume); this closes the recreation,
volume-rm, and devcontainerId-change gaps.

Kept container-private (not host bind mounts) deliberately: transcripts
can contain pasted secrets, so a host bind would spill them to host
disk, widen the supply-chain write-through surface, and leak
cross-project transcripts. A commented-out opt-in host-bind block is
included for users who accept that trade-off.

post-create.sh: chown each new volume root explicitly (find -xdev stops
at the config-volume filesystem boundary and won't descend into them),
guarded with `[ -d ] || continue` so a missing root can't abort
provisioning under set -e.

README: document the topology, what survives vs not, the one-time
first-rebuild masking of pre-existing config-volume sessions, updated
rebuild/reset commands, and the trust-boundary impact.

* feat(devcontainer): isolate host AI-CLI config via seed-once copies + persist claude-mem

Replace the read-write host bind mounts for the AI-CLI shareable dirs
(Claude skills/agents/memory/commands/plugins; Codex plugins/prompts/
memories/skills; Cursor rules/commands/agents/skills/plugins) with a
seed-once copy from a read-only /host/.<cli> stage into the per-container
config volume. The container gets its own writable copy and can never
write back to the host, closing the write-through vector where a
compromised in-container dependency could drop a malicious agent, command,
skill, or plugin onto the host for the next host session to auto-load.

Add a per-container claude-mem named volume (claude-mem-${devcontainerId})
at /home/node/.claude-mem, seeded once from a read-only /host/.claude-mem
stage. claude-mem's multi-GB SQLite + Chroma store is kept off a host bind
(unreliable fcntl locking / corruption risk over 9p on Docker Desktop
Windows) while still surviving rebuilds.

- post-create.sh: seed shareable dirs (marker-gated, seed-once) and run
  plugin-registry translation per seeded CLI; seed claude-mem behind a
  completion-sentinel guard that self-heals an interrupted multi-GB copy;
  chown the claude-mem volume only on first create.
- translate-plugin-registries.cjs: add selectRegistries() so translation
  runs per-CLI seed-once instead of clobbering container-installed plugins.
- ensure-host-config-dirs.cjs: add ~/.claude-mem; drop the shareable
  subdirs (no longer bind sources).
- devcontainer.json: drop the RW shareable binds; add the claude-mem
  volume + read-only stage.
- README: rewrite trust-boundary, mount table, and rebuild/reset docs for
  the copy model.
- tests: cover selectRegistries and the trimmed DIRS (30 pass).

* feat(devcontainer): add Bun 1.3.14, pinned via build arg

Installed by the official bun.sh/install script with the release tag
passed as the first positional arg, so the version is pinned even though
the install path itself is an unverified remote script (the one such
exception in the image — Cursor and the base image stay sha256/digest-
pinned). BUN_INSTALL is set in ENV so the binary lands at a known path
and the installer's rc-file edits don't matter. unzip is added to apt
since the Bun installer extracts a .zip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(devcontainer): persist gh auth via copy-into-volume model

Move ~/.config/gh from a read-only bind to the same read-only host
stage + per-container named volume pattern used for the AI CLI
credentials. post-create.sh seeds hosts.yml/config.yml from the
/host/.config/gh stage into the gh-config volume on create, so an
in-container `gh auth login` now persists across rebuilds while the
read-only stage still prevents any write-back to the host token.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(devcontainer): bump Claude Code to 2.1.156 for Opus 4.8

The pin was 2.1.153, which predates Opus 4.8 support (added in
2.1.154). With DISABLE_AUTOUPDATER=1 the container never updated past
the pin, so Claude Code only offered models up to 4.7. Bump to the
latest 2.1.156 so Opus 4.8 is available.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:09:01 +01:00
Zander Raycraft
fca30c7e26
fix(audit): Centralize heritage supertype matching (#1921/#1922) (#1940)
* fix(audit): Centralizes heritage supertype matching so qualified, generic, scoped, and interface bases produce inheritance edges across all OO languages, with per-language configs and fixtures.

* fix(audit): Harden parsing for #1922 with per-parse timeouts, ERROR/partial parse flags, tree-sitter pinned to 0.21.1, and CI ABI checks for every grammar.

* fix: action lint passing

* fix: feedback from triage review

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-01 13:16:17 +01:00
Gergő Magyar
d1d2a64d0f
perf(ingestion): linearize scope-capture across all languages + Python import resolution (O(n²)→O(n)) (#1918)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* bench(python-scope): build-free measure harness + baseline fingerprint for emitPythonScopeCaptures

ce-optimize scaffolding for the python-scope-capture run. Mirrors the Go
scope-capture harness (#1848): imports the .ts hotpath via tsx, times
emitPythonScopeCaptures on a synthetic DAO source at 250/800 entities, and
pins an order-independent sha256 capture fingerprint over the whole
lang-resolution/python-* corpus + a fixed 20-entity DAO as the correctness gate.

Baseline (current code) is O(n^2): 250->800 entities (3.2x) -> 10.7x time
(1062->11343ms), scaling_ratio 3.34.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* optimize(python-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks

emitPythonScopeCaptures re-derived each tree-sitter match's AST node via
findNodeAtRange(tree.rootNode, ...) on every match, scanning all of root's named
children per call -> O(matches x rootChildren) ~ O(n^2). The same #1848 bug Go
had (fixed in eaf0a305), mirrored in Python's captures.ts.

Thread the query-captured SyntaxNode (c.node) through a parallel tag->node map
and use it directly for all three sites (import / @scope.function /
@declaration.function). The Python scope query captures the full
statement/definition node, so the captured node IS the one the old code
re-derived by range — no ancestor walk needed (simpler than Go's import case).

Output is byte-identical: an order-independent sha256 capture fingerprint over
all 188 lang-resolution/python-* fixtures + a 20-entity DAO is unchanged.
800 entities: 11343ms -> 319ms (35.5x); 250: 1063ms -> 95ms (11.2x);
scaling_ratio 3.34 -> 1.05 (quadratic -> linear). tsc clean; 291 python
scope-resolution + resolver tests pass.

Adds a golden capture-parity test (forward-drift guard across the python-*
corpus + DAO shape) and a non-gated O(n^2) regression tripwire (400-entity
source, 346ms vs a 10s budget).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* optimize(python-scope-capture): index Python import resolution to kill O(imports x files) scans

resolvePythonImportTarget's fallback path scanned the entire repo file set on
every unresolved/external dotted import — once in hasRepoCandidate (package gate)
and once in resolveAbsoluteFromFiles (suffix match) — giving O(imports x files)
~ O(n^2) in the resolution phase (audit follow-up to the capture-phase #1848
mirror).

Add a per-file-set index (byBasename buckets + .py dir-prefix set + normalized
path set), memoized on the allFilePaths Set via a WeakMap so it is built once per
run and reused across every import. The two O(files) scans become O(1)/O(bucket)
lookups. The shared buildSuffixIndex is deliberately NOT reused: it keeps only a
single path per suffix (longest wins) and cannot reproduce Python's exact
fewest-segments-then-lexicographic tie-break across all candidates (see the
import-target.ts:72 rationale) — so a purpose-built index is used instead.

Output is identical: a resolver-output fingerprint over 10,021 cases (exhaustive
branch matrix — tie-breaks, gating, collisions, windows paths — plus a 400-repo
deterministic fuzz) is byte-for-byte unchanged
(e6ec1a59...). Worst-case scaling (k imports x k files): 500/1000/2000/4000 went
25/62/231/899ms -> 1.2/2.9/6.7/10.7ms (84x at 4000, quadratic -> linear).

tsc clean; 303 python scope-resolution + resolver tests pass; adds a 10-case
parity guard pinning the tie-break / gating / collision semantics the index
must preserve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(python): land the import-index reuse on the registry-primary path (PR #1918 P1)

The PythonFileIndex WeakMap is keyed on allFilePaths Set identity, but
pythonScopeResolver.resolveImportTarget wrapped the orchestrator's stable
run-level set in `new Set(allFilePaths)` per import, handing a fresh key to
every import — so the index rebuilt on every import and the O(imports x files)
cost this index removed persisted on the production path (PR #1918 review P1).

Thread ReadonlySet<string> through the resolver chain (PythonResolveContext,
getPythonFileIndex, the WeakMap key, resolveAbsoluteFromFiles, hasRepoCandidate,
resolvePythonImportInternal, tryResolveWithExtensions — all read-only) and drop
the per-import copy so the stable set reaches the WeakMap key. Mirrors the C#
counterpart (csharp/import-target.ts), which already keys on ReadonlySet.

Guard it deterministically: an ungated index-build counter (index-stats.ts) +
a production-path integration test that drives pythonScopeResolver over 300
imports on a stable set and asserts the index is built ONCE (was 300 pre-fix).

tsc clean; resolver-output fingerprint unchanged (e6ec1a59); 369 python
scope-resolution + resolver tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(python): index only .py files in the import-resolution index (PR #1918 P3b)

getPythonFileIndex pushed every workspace file into byBasename (and normSet),
but Python import resolution only ever queries .py paths — module <seg>.py,
package <seg>/__init__.py, and .py directory prefixes. Non-.py files (.ts, .go,
…) could never match any lookup, so they were pure dead weight in the index on
polyglot monorepos.

Skip non-.py files at the top of the index builder. dirPrefixes was already
.py-gated; this extends the same guard to byBasename and normSet (both also
.py-only consumers), so it is behavior-preserving. Resolver fingerprint
unchanged (e6ec1a59); adds a polyglot parity case proving .ts/.go siblings
never affect resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(python): parent-key the __init__ bucket to kill package-count skew (PR #1918 P2b)

The suffix fallback's package form looked up byBasename.get('__init__.py'),
which holds every __init__.py in the repo — so every multi-segment package
import (pkg.sub) iterated all N packages to find the one ending /sub/__init__.py.

Add byInitParent: __init__.py files keyed by their last two components
(<parentDir>/__init__.py). The package lookup now targets only same-named
package dirs (typically O(1)) and confirms the full suffix, so the final
candidate set and tie-break are unchanged. __init__.py files stay in byBasename
too, so the rarer explicit "pkg.__init__" import still resolves via the module
(<lastSeg>.py) lookup.

Resolver fingerprint unchanged (e6ec1a59); adds parity cases for a nested
package (same-parent noise filtered by the suffix confirm) and an explicit
pkg.__init__ import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(python): reproduce old startsWith gating for absolute paths + re-baseline (PR #1918 P3a)

getPythonFileIndex built dirPrefixes by split('/')+filter(Boolean), which drops
the leading empty component of an absolute path: "/repo/svc/x.py" yielded
{repo/, repo/svc/}. The old full-scan gate compared the whole normalized path,
where "/repo/svc/x.py".startsWith("repo/svc/") is false — so the index gate
PASSED where the old gate BLOCKED, an absolute-path-only divergence (production
paths are repo-relative, so this never fired in production).

Build dirPrefixes from every slash-terminated prefix of the full path instead
(including the leading "/" for absolute paths), so dirPrefixes.has(X) matches
exactly when the old f.startsWith(X) did. For repo-relative paths the prefix set
is identical, so production behavior is unchanged.

This is NOT cosmetic. Extending the fingerprint harness with absolute-path file
sets surfaced 12 fuzz cases (out of ~4000 new absolute cases) where the pre-fix
index resolved an import the old code left unresolved — e.g. `pkg.thing` over
{/repo/pkg/__init__.py, /repo/vendor/pkg/thing.py} from /repo/app/main.py
resolved to /repo/vendor/pkg/thing.py under the buggy gate but is null (old and
fixed). The fix removes those absolute-path false positives.

Re-baseline justification: the committed resolver fingerprint moves
e6ec1a59 -> d51ea9ed because the harness now adds ~4000 absolute-path cases
(branch matrix incl. the reviewer's exact case + a 200-repo absolute fuzz). The
relative-path subset is unchanged: the original 10,021-case relative corpus
still hashes to e6ec1a59 after the dirPrefixes fix (the fix only alters
absolute-path prefixes). The new baseline encodes the old-startsWith-equivalent
(correct) behavior, verified by diffing the fixed vs. pre-fix harness output.

Adds parity cases pinning the absolute false-positive (now null) and a
repo-relative control of the same shape (still resolves). tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(python-bench): add --check mode + REPS=7 to the scope-capture harnesses (PR #1918 P2a)

The bench harnesses were dev-only — nothing compared the committed fingerprints
or guarded the scaling, so an O(n^2) regression (or a P1-style cache miss) could
land silently.

Add a --check mode to both:
- measure.mjs: assert the capture fingerprint == baseline-fingerprint.txt AND
  scaling_ratio < 1.5 (linear), exit non-zero on either. REPS bumped 3 -> 7 to
  stabilize the median on shared CI runners.
- import-target-fingerprint.mjs: assert the resolver fingerprint ==
  baseline-import-target-fingerprint.txt, exit non-zero on drift.

Without --check both still print JSON for dev use / deliberate re-baselining.
Verified: --check passes on the current tree (capture f2b4376f / scaling 1.04;
resolver d51ea9ed) and exits 1 with a clear message on a corrupted baseline.
Wired into CI by the dedicated benchmark job (next commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(bench): add a dedicated benchmark job wiring in the gated cross-language suites

The cobol/csharp/rust/php/ruby *-pipeline-benchmark.test.ts suites are gated
behind GITNEXUS_BENCH, so the main coverage job skips them — their O(n^2)
scaling guards never actually ran in CI. Add a dedicated "benchmarks" job to the
Tests reusable workflow that runs them with GITNEXUS_BENCH=1, plus the Python
scope-capture and import-resolution fingerprint + scaling guards
(measure.mjs --check, import-target-fingerprint.mjs --check) from PR #1918.

Runs with --no-file-parallelism: the suites measure wall-clock and peak heap, so
parallel forks both skew the timings and OOM the worker pool (reproduced locally:
the parallel run crashes a worker; serial passes 5/5 in ~80s). The job is part of
the Tests workflow, so it gates the existing CI Gate required check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(bench): exclude go-pipeline-benchmark from the gated job (fork-pool instability)

Validation surfaced that go-pipeline-benchmark.test.ts's worker-pool (#1848)
suite spins a real worker pool that exits unexpectedly under vitest's fork pool,
crashing the run (1 of 3 tests, repeated). Including it would make the new
benchmark gate flaky. The other five language pipeline benchmarks
(cobol/csharp/rust/php/ruby) run clean serially (5/5, ~84s). Go is already
guarded by its non-gated O(n^2) tripwire (main coverage job) + golden parity
test, so coverage is preserved. Documented inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): set persist-credentials false on all ci-tests checkouts (zizmor artipacked)

The new benchmarks job (and the pre-existing tests / cross-platform jobs) used
actions/checkout with the default persist-credentials, leaving the token in
.git/config. The tests job uploads a test-reports artifact, so that is the
literal credential-persistence-through-artifacts case zizmor's artipacked audit
flags; the others persist creds needlessly.

None of these jobs push — they run npm + vitest only — so persist-credentials:
false is safe (the packaged-install-smoke job already runs setup-gitnexus this
way). All four ci-tests.yml checkouts are now consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bench(scope-capture): unified build-free measure harness for all benchmarked languages

Adds a single tsx harness that measures emit<Lang>ScopeCaptures for every
language with a pipeline benchmark (go, csharp, rust, php, ruby, cobol):
per-language synthetic-DAO scaling (250/800 entities) + an order-independent
sha256 fingerprint over each <lang>-* fixture corpus, with a --check mode gating
both against baselines.json.

It immediately surfaced that csharp, rust, php and ruby still carry the
O(matches x rootChildren) findNodeAtRange(tree.rootNode,...) root-walk that was
fixed for go (#1915) and python (#1918): scaling ratios 3.13 / 3.31 / 3.04 /
3.07 (vs ~1.0 for the fixed go and cobol). They are flagged known_quadratic in
baselines.json so CI guards drift + worsening until each gets the threaded-node
fix (following commits).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(ruby): linearize scope-capture (thread captured nodes + dedup set)

emitRubyScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration.function /
heritage / attr / call-arity), and the constructor-return pass ran out.some(...)
once per method over the growing output array — two O(n^2) shapes (measured
scaling 3.07).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), and precompute the YARD-return
dedup keys into a Set. Output byte-identical (capture fingerprint over the
ruby-* fixture corpus + DAO unchanged); scaling 3.07 -> 1.11 (linear). 127 ruby
resolver tests pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(php): linearize scope-capture (thread captured nodes)

emitPhpScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration / call-arity),
giving O(matches x rootChildren) ~ O(n^2) (measured scaling 3.04).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the php-* fixture corpus
+ DAO unchanged); scaling 3.04 -> 1.03 (linear). 205 php resolver tests pass;
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(rust): linearize scope-capture (thread captured nodes)

emitRustScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration / type-binding
return-hoist / call-arity), giving O(matches x rootChildren) ~ O(n^2) (measured
scaling 3.31 — the worst of the four).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the rust-* fixture corpus
+ DAO unchanged, incl. the impl-block return-type hoist path); scaling
3.31 -> 1.05 (linear). Rust resolver tests pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(csharp): linearize scope-capture (thread captured nodes)

emitCsharpScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match at 7 sites (import / read.member / scope.function /
declaration / call-arity / primary-constructor class+record), giving
O(matches x rootChildren) ~ O(n^2) (measured scaling 3.13).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the csharp-* fixture
corpus + DAO unchanged); scaling 3.13 -> 0.99 (linear). C# resolver tests pass;
tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(bench): tighten scope-capture budgets to linear + gate all 6 languages in CI

All six benchmarked languages now thread the captured node, so update
baselines.json: drop known_quadratic and set scaling_budget 1.5 (linear) for
csharp/rust/php/ruby (go/cobol already linear). Fingerprints are unchanged —
every fix was byte-identical.

Wire the unified build-free guard into the benchmarks job:
'node --import tsx bench/scope-capture/measure.mjs --check' asserts the capture
fingerprint and linear scaling for go/csharp/rust/php/ruby/cobol on every run.
Build-free (no worker pool), so unlike the go pipeline benchmark it is stable in
CI. measure --check passes locally for all six (scaling 0.86-1.10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ingestion): address PR #1918 tri-review — shared nodeIfType, duck-typed guard, docs

Tri-review follow-ups (no behavior change — all capture fingerprints + the
resolver fingerprint are byte-identical, verified via the bench --check gates):

- maintainability (M1): extract the `nodeIfType` helper (copy-pasted into 4
  captures.ts files) to ast-helpers.ts as a generic `nodeIfType<T extends
  SyntaxNode>`. csharp/php keep their local SyntaxNode aliases (used elsewhere);
  the generic signature accepts them.
- P2 (latent): duck-type the `resolvePythonImportTarget` shape-guard instead of
  `instanceof Set`. The context type was widened to ReadonlySet<string>; an
  `instanceof Set` check would reject a legitimate non-Set ReadonlySet and
  silently drop all Python import edges. Now checks `.has` + `[Symbol.iterator]`.
- P3 (ruby dedup): document the snapshot-vs-live `out.some`→Set behavior — the
  one narrow corner (two same-named methods one row apart, both ending in
  Const.new) where output differs from the pre-PR code, and why the new
  behavior (emit both) is intended.
- harness cross-ref: note in python-scope/measure.mjs that Python's capture
  scaling is guarded there (not the unified scope-capture harness) so neither
  is removed assuming the other covers Python.

tsc clean; scope-capture --check passes (6 languages, unchanged + linear);
resolver fingerprint unchanged; 300 python/ruby/rust tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ingestion): golden + O(n^2) tripwire tests for ruby/rust/php/csharp scope-capture

Addresses the PR #1918 tri-review test-gap consensus (testing + adversarial +
maintainability): the four newly-linearized languages had no committed
correctness/scaling lock in the standard unit-test job — only the
bench/scope-capture/measure.mjs --check fingerprint, which runs in the separate
benchmarks CI job.

Per language, mirroring the existing go/python tests:
- test/unit/scope-resolution/<lang>/<lang>-captures-golden.test.ts — ORDER-
  SENSITIVE golden (modeled on go-captures-golden.test.ts; catches emission
  reordering the order-independent bench fingerprint misses) over the whole
  lang-resolution/<lang>-* corpus + a 20-entity synthetic DAO, with UPDATE_GOLDEN
  regeneration. Runs in the normal unit-test job (fast-fail).
- test/integration/<lang>-scope-capture-tripwire.test.ts — non-gated O(n^2)
  regression tripwire (400-entity source, <10s budget), like python's.

The ruby golden also pins the snapshot-dedup behavior (two same-named methods
both ending in Const.new emit BOTH @type-binding.return bindings — PR #1918 P3),
and the rust golden exercises the impl-block return-type hoist path.

41 tests pass; tsc clean. Goldens generated against the (byte-identical) current
output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:44:22 +01:00
dependabot[bot]
ca95df6316
chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 (#1866)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](68bde559de...9e0d7b8d25)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 06:46:18 +01:00
Gergő Magyar
ac9a2ee12f
chore(ci): consolidate parity shards and narrow cross-platform matrix (#1798)
* chore(ci): reduce CI runner-minutes by consolidating parity and narrowing cross-platform

Scope-resolution parity previously spawned 9 separate GitHub Actions jobs
(one per migrated language), each doing full checkout + npm ci + build for
a single test file. Consolidate into one job running scripts/run-parity.ts
which loops through all migrated languages sequentially — same coverage,
~45 fewer runner-minutes of redundant setup per PR.

Cross-platform (Windows/macOS) previously ran the full 373-file test suite.
Narrow to 45 platform-sensitive files (native LadybugDB, process spawning,
path separators, worker threads, filesystem behavior). Full suite still runs
on Ubuntu with coverage.

Also adds 2 missing lbug integration tests (lbug-orphan-sidecar-recovery,
lbug-readonly-init) to the sequential lbug-db vitest project where they
belong, and rewrites TESTING.md to document all test lanes.

* fix: address code review findings on parity and cross-platform scripts

- Capture stderr in run-parity.ts (vitest writes diagnostics to stderr)
- Lower per-invocation timeout from 5min to 60s to stay within CI job limit
- Add --language flag validation (error on missing value)
- Add timeout diagnostic to run-cross-platform.ts catch block
- Add analyze-wal-checkpoint-failure.test.ts to lbug-db sequential project
- Expand cross-platform list: parser-loader, pipeline, pipeline-graph-golden,
  setup-skills, cli/tool-no-index-stderr (51 files, was 45)

* fix: add shell:true for Windows npx resolution and simplify fs import

execFileSync('npx', ...) fails with ENOENT on Windows because npx is
npx.cmd — shell:true resolves this. Also replaces dynamic await
import('fs') with static import, and fixes timeout detection to use
err.killed instead of err.code.

* fix(ci): raise parity per-invocation timeout to 120s and job timeout to 30min

TypeScript and C++ resolver tests take 60-90s on CI runners, exceeding
the 60s per-invocation timeout. Raise to 120s. Also bump the job-level
timeout from 25 to 30 minutes for margin (realistic total is ~11 min).

* fix(ci): raise parity per-invocation timeout to 180s for C++ resolver

C++ resolver tests take 130-150s on CI runners due to template
metaprogramming, ADL, and SFINAE fixture volume. 120s was still too
tight. Realistic total across all 9 languages is ~12 min, well under
the 30-min job timeout.

* fix(ci): use stdio inherit for parity — no per-invocation timeout

Switch from piped stdio with per-invocation timeouts to stdio: 'inherit'.
Vitest output streams to CI console in real time, making failures
immediately visible. The CI job-level timeout (30 min) is the only
guard — no more artificial per-invocation timeouts that cut off slow
resolver tests like C++ (which genuinely takes 3+ minutes).

---------

Co-authored-by: Test <test@example.com>
2026-05-24 12:10:10 +01:00
Gergő Magyar
d3de5fa5d5
fix(install): materialize vendored grammars to fix Windows EPERM (#1728) (#1729)
* fix(install): materialize vendored grammars to fix Windows EPERM (#1728)

Stop using file: optionalDependencies for tree-sitter-dart/proto/swift,
which made npm symlink vendor paths on install and fail on Windows without
symlink privileges. Copy vendor trees into node_modules at postinstall
instead; keep native builds and #836 vendor hygiene.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(install): atomic materialize swap + fail-soft tests (#1728, #836)

Hardens PR #1729 against two issues the original implementation could
still hit:

1. Torn-state on rmSync→cpSync. The previous loop deleted the
   destination before copying. If cpSync threw — the exact Windows EPERM
   scenario this PR targets — a previously-working grammar was silently
   wiped. Now we copy to {dest}.materialize-tmp first and renameSync into
   place, so an interrupted copy leaves the prior materialization intact.

2. Fail-soft try/catch had no test coverage. Adds two POSIX-only tests
   (chmod 0o555 to deterministically force cpSync to throw) that verify
   (a) a single grammar failure does not abort the other two, and (b) an
   existing materialization survives a partial-copy failure. Skipped on
   Windows where chmod doesn't enforce write restriction; runs on Linux
   CI.

Other test improvements locking in the install-hygiene invariants:

- All three vendored grammars (dart/proto/swift) checked, not just dart.
- GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 short-circuit is exercised.
- Vendor cleanliness (#836): no node_modules/build under vendor/.
- Idempotent re-runs (clean overwrite verified via sentinel file).
- Missing-vendor warn+continue path now has explicit coverage.
- Vendored package manifests asserted to carry no install script or
  runtime dependencies.
- package.json optionalDependencies asserted free of vendored grammars.
- package-lock.json assertion tightened from `if (entry !== undefined)
  { expect(entry.link).not.toBe(true); }` (vacuous when entry is absent,
  i.e. the expected post-fix state) to `expect(...).toBeUndefined()`.

Verified locally:
- npx tsc --noEmit: clean
- vitest test/unit/materialize-vendor-grammars.test.ts: 8 pass + 2
  POSIX-only skipped on Windows
- npm pack tarball: no vendor/*/node_modules or vendor/*/build entries
- Isolated global install (clean + upgrade + SKIP env) into temp prefix:
  succeeds; gitnexus --version → 1.6.5; vendor stays clean post-install.

* fix(install): address review feedback — Swift parity, atomicity, CI smoke

Resolves all findings from the automated production-readiness review on
verify/issue-1728-symlink.

Swift warning parity (review #2):
  Add tree-sitter-swift to OPTIONAL_GRAMMARS in src/cli/optional-grammars.ts
  alongside Dart and Proto. Before this commit, Swift was materialized at
  postinstall and probed by build-tree-sitter-swift.cjs but the runtime
  warnMissingOptionalGrammars() never warned when it failed to load —
  users got silent Swift degradation from the optional-grammars surface
  (parser-loader's separate unavailableNote only fires on demand). Now
  the warning path matches the materialize path.

README env-var table (review #1):
  Update the GITNEXUS_SKIP_OPTIONAL_GRAMMARS row at README.md line 248 to
  list all three vendored grammars (dart, proto, swift). The quick note
  earlier in the README already mentioned all three; only the table row
  was stale.

Atomicity hardening (review #3):
  materialize-vendor-grammars.cjs now copies to {dest}.materialize-tmp,
  renames the existing dest to {dest}.materialize-bak (if present), then
  renames the partial into dest, then removes the backup. If the
  partial→dest rename fails (e.g. Windows AV scanner racing the swap),
  the catch block restores from backup so the previously-materialized
  grammar is preserved. Closes the narrow torn-state window where the
  prior implementation could leave dest deleted after rmSync succeeded
  but renameSync failed.

Swift probe docs (review #4):
  build-tree-sitter-swift.cjs script header rewritten to describe what
  the script actually does — probe node-gyp-build at install time so
  missing-prebuild failures surface as install-time warnings instead of
  first-parse runtime errors. The script does not "activate" anything;
  the runtime require() in parser-loader does the actual load. Console
  warning text updated to match ("prebuild probe" not "activation").

Windows packaged-install smoke test (review #5):
  New CI job `packaged-install-smoke` in .github/workflows/ci-tests.yml
  matrices on windows-latest and ubuntu-latest. Runs npm pack, installs
  the produced tarball globally into RUNNER_TEMP, then asserts:
    * no vendor/*/node_modules or vendor/*/build (#836 invariant)
    * tree-sitter-{dart,proto,swift} in node_modules are real
      directories, not junctions/symlinks (#1728 invariant)
    * gitnexus --version runs against the installed CLI
  Closes the coverage gap where the existing windows-latest job only
  ran `npm ci` in the source checkout — exercising postinstall but not
  the tarball reify step that historically tripped EPERM.

Verified locally:
  npx tsc --noEmit: clean
  vitest test/unit/materialize-vendor-grammars.test.ts test/unit/cli-commands.test.ts:
    18 pass + 2 POSIX-only skipped on Windows
  prettier + eslint on all changed files: clean

* fix(ci): disable credential persistence on packaged-install-smoke checkout

GitHub Advanced Security (zizmor artipacked) flagged the new
packaged-install-smoke job's actions/checkout step as a potential
credential-persistence risk. The job runs `npm pack` + global install
and never pushes back, so the GITHUB_TOKEN that checkout would persist
in .git/config provides no value and only widens the leak surface (any
future artifact-upload step in this job would carry the token).

Disable persistence explicitly via `persist-credentials: false` on this
job's checkout. Scoped to the new job — pre-existing checkouts above
are left unchanged.

* fix(ci): use find instead of ls for tarball lookup (SC2012)

actionlint shellcheck SC2012 flagged `TARBALL=$(ls gitnexus-*.tgz | head -n1)`.
Switch to `find . -maxdepth 1 -name 'gitnexus-*.tgz' -print -quit` which
handles non-alphanumeric filenames safely. Also add an explicit
empty-result check so the failure mode is a clear error message instead
of a silent `npm install -g ""` later.

* fix(tests): sabotage vendor src (not partial path) in POSIX fail-soft tests

The fail-soft tests in materialize-vendor-grammars.test.ts pre-chmod'd
the destination's .materialize-tmp partial directory to 0o555 to force
cpSync to throw. After the atomicity rewrite (`fix(install): atomic
materialize swap + fail-soft tests`), the materialize script now starts
each grammar's loop with `fs.rmSync(partial, { force: true })`, which
deletes the chmod'd sabotage before cpSync runs — so cpSync succeeds and
the partial is then renamed into dest, leaving the test's `finally`
block with no path to chmod back (ENOENT) and the assertion that proto
remained unmaterialized failing because it materialized cleanly.

Fix: sabotage the *vendor source* directory (which the script reads from
but never modifies) by chmod'ing it to 0o000. cpSync then fails on
readdir, the catch block fires per-grammar, dart and swift still
materialize from their unaffected sources, and the existing-dest
preservation test verifies that a sabotaged second-run leaves the prior
materialization (and its sentinel file) intact.

Tests now pass locally (8 pass + 2 POSIX-only skipped on Windows) and
should pass on macOS/Ubuntu CI where the sabotage runs.

* fix(tests): restrict fail-soft tests to Linux (macOS Node cpSync abort)

Node 22 on macOS aborts the process with `libc++abi: terminating due
to uncaught exception filesystem_error` when fs.cpSync hits a source
directory it can't read — the abort happens at the C++ filesystem layer
and bypasses Node's JS try/catch entirely (nodejs/node#51399). My
chmod-0o000-the-source sabotage strategy triggers this SIGABRT on
macOS CI before the production script's `try { cpSync } catch` ever
runs, so the test sees a child-process crash instead of the fail-soft
warning it's verifying.

The production script's fail-soft is correct on Linux (where EACCES
surfaces as a normal JS exception) and effectively untestable on macOS
via permission sabotage. Real installs don't hit this — npm always
ships vendor/ with readable permissions — so the macOS gap is a test
artifact, not a behavior gap.

Restrict the two chmod-based tests to Linux only by replacing
`skipOnWin` with `linuxOnly`. Linux CI continues to verify both the
one-grammar-fails-others-succeed and existing-materialization-preserved
invariants. macOS and Windows runs skip these two scenarios; the other
8 tests still run on every platform.

* fix(tests): remove materialize unit tests, rely on CI smoke job

The materialize-vendor-grammars.test.ts file has been a recurring source
of platform-specific CI noise:

  - Windows: chmod doesn't enforce read/write restrictions the way POSIX
    does, so the fail-soft tests had to be skipped there.
  - macOS Node 22: cpSync against an unreadable source aborts the process
    with a libc++ filesystem_error (nodejs/node#51399) that bypasses JS
    try/catch entirely — making the chmod-based fail-soft tests
    unrunnable on macOS too.
  - The "vendor-cleanliness" and "idempotency" tests on Windows
    intermittently flake due to fs.cpSync timing on the GitHub runner.

The invariants these tests verified are now covered by stronger,
more realistic surfaces:

  - packaged-install-smoke (ci-tests.yml): runs `npm pack` then
    `npm install -g ./gitnexus-*.tgz` on windows-latest and
    ubuntu-latest, then asserts no vendor/*/node_modules,
    no vendor/*/build (#836), no junctions/symlinks on the
    materialized grammar directories (#1728), and a working
    `gitnexus --version`. This is the actual end-user install path.

  - cli-commands.test.ts (kept, unmodified): asserts package.json
    declares no `file:` optionalDependencies for vendored grammars,
    the Swift vendor manifest carries no install script or
    dependencies, and the postinstall chain runs
    materialize-vendor-grammars.cjs + build-tree-sitter-swift.cjs.
    These are static manifest checks — deterministic, fast, no
    flake risk.

Removing the dynamic script-execution tests trades unit-level coverage
for end-to-end smoke coverage that actually exercises the
`file:` → cpSync change against a real npm install lifecycle, on
the platform the fix targets (windows-latest).

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 16:47:22 +01:00
dependabot[bot]
061e123d72
chore(deps): bump actions/dependency-review-action from 4.9.0 to 5.0.0 (#1739)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.9.0 to 5.0.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](2031cfc080...a1d282b36b)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 12:07:22 +01:00
dependabot[bot]
a4954368ad
chore(deps): bump release-drafter/release-drafter from 7.2.1 to 7.3.0 (#1740)
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 7.2.1 to 7.3.0.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](563bf13265...c2e2804cc5)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 12:07:07 +01:00