* feat(cpp): SFINAE-aware overload filter — drops candidates whose enable_if_t / requires constraints fail (#1579)
* fix(cpp): SFINAE follow-ups for is_integral_v/is_arithmetic_v bool and char support, an unqualified F1 test fixture, and parameter-lookup gap documentation (#1579) -> claude feedback
* revert: reverting all changes to .md files
* feat(cpp): add standard-conversion-sequence ranking to overload resolution (#1578)
Introduce `ConversionRankFn` abstraction and `cppConversionRank` implementation
to disambiguate C++ overloaded calls by argument-to-parameter conversion cost.
Exact type match (rank 0) beats standard arithmetic conversion (rank 2), which
beats non-viable mismatch (Infinity). Thread the rank function through
`narrowOverloadCandidates`, `pickImplicitThisOverload`, `pickOverload`, and
`pickUniqueGlobalCallable` via the `ScopeResolver.conversionRankFn` contract.
Add `findAllCallableBindingsInScope` scope walker for collecting all overloads
at the first binding scope. Guard against false ambiguity suppression when
candidates span different files (local-shadows-import preservation).
* fix: address Claude review findings on conversion-rank PR
Finding 1 (HIGH): add tests that exercise the conversion ranker.
- p('a') with p(int)/p(double): char→int promotion (rank 1) beats
char→double conversion (rank 2), forcing step 4b in
narrowOverloadCandidates. Exact-type filter misses both overloads.
- h(42, 2.5) with h(int,int)/h(double,double): multi-arg tied total
score forces the ranker, both candidates score 2 → suppressed.
Finding 2 (HIGH): unify multi-candidate suppression across all paths.
- Non-ADL free-call: suppress when narrowed.length > 1 (same-file
guard), mirroring ADL merged-candidate behavior.
- ADL ordinary-only: same pattern.
- pickOverload: return OVERLOAD_AMBIGUOUS when candidates.length > 1
after normalized-ambiguity check.
- Case 0.5 (this receiver): set ambiguous=true when narrowed > 1.
Finding 3+4 (MEDIUM): implement rank-1 integral promotions.
- char→int and bool→int now return rank 1 (ISO C++ [conv.prom]).
- Updated comment to remove misleading ISO table header; document
only the post-normalization ranking that is actually implemented.
- Updated ConversionRankFn JSDoc in overload-narrowing.ts.
218/218 C++ tests pass (registry-primary). Legacy: 186+32.
* fix: implement pairwise dominance comparison for overload ranking
Replace the summed per-slot conversion cost with ISO C++-aligned
pairwise dominance comparison ([over.ics.rank]). F1 is better than
F2 only when F1 is not worse for every argument and strictly better
for at least one. Non-dominated candidates are returned; if multiple
remain they are genuinely ambiguous.
This fixes false CALLS edges for asymmetric multi-arg overloads:
h('a', 2.5) against h(int,int) / h(double,double) — the old summed
cost picked h(double,double) (cost 2 < 3), but ISO C++ considers
the call ambiguous because h(int,int) is better at arg 0 via char
promotion. The pairwise check correctly finds neither dominates.
Add h('a', 2.5) test case asserting zero CALLS edges alongside
the existing h(42, 2.5) symmetric-tie test.
218/218 C++ tests pass (registry-primary). Legacy: 186+32.
* docs: update step 4b JSDoc to reflect pairwise dominance
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* Initial plan
* fix: add time-based deadline to cross-file type propagation to prevent stalling on large repos
Adds a 2-minute wall-clock time limit (DEFAULT_CROSS_FILE_ELAPSED_MS) to
runCrossFileBindingPropagation. When exceeded, the phase gracefully stops
and logs a warning. Users can override via GITNEXUS_CROSS_FILE_TIMEOUT_MS
env var. This prevents the analyze command from stalling for hours on very
large repositories where per-file re-resolution is expensive.
Fixes the reported issue where gitnexus analyze stalls at "Cross-file type
propagation" for several hours on repos with 15000+ files.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b8341947-557c-4111-a3a8-991ba455ab01
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: root cause - cache tree-sitter queries across files, add live progress reporting
Root cause: cross-file propagation called processCalls() with 1 file at a time,
causing Parser.Query to be recompiled from the query string for every single file
(O(N) compilations vs O(1) for the whole phase). Additionally, progress was only
reported once at the start, making the phase appear completely frozen.
Fixes:
- Add optional `compiledQueryCache` parameter to `processCalls` so callers that
invoke it with single-file batches can share compiled query objects across calls.
The cross-file phase now compiles each language's query string exactly once and
reuses it for all files of that language (e.g. 1 TypeScript compile for 595+ files).
- Pre-count candidate files and emit onProgress every 25 files showing
"Cross-file type propagation (N/M files)..." so the UI shows real movement
instead of a frozen bar.
- Keep the wall-clock deadline (GITNEXUS_CROSS_FILE_TIMEOUT_MS) as a safety
net for pathological inputs.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f5028cc8-4bc9-4309-8ffb-798fe2bd7a0a
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: address code review - use SupportedLanguages key type, rename queryCache to compiledQueryCache
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f5028cc8-4bc9-4309-8ffb-798fe2bd7a0a
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(cross-file): remove wall-clock timeout from type propagation
The query compilation cache and live progress reporting address the
original stall; the 2-minute deadline could truncate cross-file work on
large repos. MAX_CROSS_FILE_REPROCESS (2000) remains as the only cap.
* test(cross-file): verify compiledQueryCache is shared across all processCalls invocations
Finding 1: O(N) query recompilation was fixed by sharing a compiledQueryCache Map
across all processCalls invocations in runCrossFileBindingPropagation. This test
verifies the fix is correctly wired: the same Map instance is passed as the
12th argument to every call, proving queries are compiled once per language,
not once per file.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3ab768d9-3993-4882-9d8f-17f7fcbd086e
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cross-file): verify live progress events are emitted with N/M format
Finding 2: frozen progress display was fixed by emitting onProgress every 25 files
with "Cross-file type propagation (N/M files)..." messages instead of calling it
once at phase start. This test verifies the fix with 50 candidate files: expects
onProgress called 3 times (1 initial + at 25 + at 50) with correct N/M counters.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3ab768d9-3993-4882-9d8f-17f7fcbd086e
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(cross-file): skip registry-primary language files before readFileContents
Finding 3 (from comment 4466231612): cross-file-impl was calling processCalls
for every candidate file even when that file's language is registry-primary
(TypeScript, C++, Python, Go, C#, PHP, C — since AGENTS.md v1.7.0). processCalls
would immediately skip those files via its own isRegistryPrimary guard, but
cross-file-impl still paid the full cost: readFileContents I/O, buildImportedReturnTypes,
buildImportedRawReturnTypes, and Map allocation — all discarded.
Fix: check isRegistryPrimary(lang) in both the totalCandidates pre-count loop
and the levelCandidates builder, before any file I/O or map building. This
eliminates 595+ no-op processCalls invocations on large TypeScript repos.
Test: mocks isRegistryPrimary to always return true and verifies that
processCalls is never invoked and result is 0. The mock also defaults to false
in beforeEach so existing tests using .ts files are unaffected.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3ab768d9-3993-4882-9d8f-17f7fcbd086e
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* refactor(test): address code review - simplify mock factory, name the arg index constant
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3ab768d9-3993-4882-9d8f-17f7fcbd086e
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
PR #1627's npm install -g npm@latest step crashed mid-install with MODULE_NOT_FOUND: promise-retry — a known fragility when npm self-upgrades. Node 22's bundled npm is 10.9.x (no OIDC). Fix: bump publish job's node-version to 24, which ships with npm 11.x natively. Package consumers unaffected (this Node version is only used during publish; engines.node is >=22.0.0; ci-tests.yml continues testing on Node 22).
First live-fire RC publish after #1610 failed at npm publish with E404. The if: failure() cleanup correctly auto-deleted the partial v-tag and rc-marker, but OIDC never engaged. Root cause: two coordinated upstream bugs.
1. actions/setup-node@v6 with registry-url: writes _authToken into the runner .npmrc AND exports NODE_AUTH_TOKEN from its token: input (defaulting to github.token). npm publish sends GITHUB_TOKEN as the bearer and the registry returns 404. OIDC never tried because npm thinks it already has a credential. See actions/setup-node#1440.
2. The Node 22 runner ships with npm 10.9.x. npm Trusted Publishing OIDC support requires npm >= 11.5.1.
Fix: omit registry-url: from the setup-node step (per the consensus workaround in community discussion #176761), and add npm install -g npm@latest before publish. --provenance flag is NOT added; npm auto-attaches provenance under Trusted Publishing.
Sources:
- https://github.com/actions/setup-node/issues/1440
- https://github.com/orgs/community/discussions/176761
- https://docs.npmjs.com/trusted-publishers/
Collapse release-candidate.yml into publish.yml so there is exactly one workflow that publishes gitnexus to npm, creates GitHub Releases, and triggers Docker builds — for both release candidates and stable releases. Closes#1609 architecturally.
A first-stage `route` job classifies push-to-main / push-tag / workflow_dispatch into `rc` / `stable` modes and fails closed on malformed shapes. RC path runs rc-guard → ci.yml → publish (mint GitHub App token → checkout with persist-credentials:false → resolve next rc version → atomic v-tag + rc/<SHA> marker push → vtag integrity gate → npm publish via OIDC → GitHub prerelease → if: failure() cleanup) → docker.yml. Stable path verifies package.json matches the tag and publishes to `latest` via OIDC (no docker).
Hardening:
• Self-trigger prevention via negative-glob `tags: ['v*', '!v*-rc.*']` — the bug class behind #1609 cannot recur.
• Two distinct actions/checkout steps per mode (no conditional `token:` expression footgun).
• Workflow-level `permissions: {}` deny-all + per-job grants; `id-token: write` only where OIDC is used.
• npm Trusted Publishing replaces NPM_TOKEN (delete the secret after the first successful publish).
• GitHub App installation token (actions/create-github-app-token@v3.2.0) replaces the long-lived RELEASE_PUSH_TOKEN PAT (delete after first successful RC).
• vtag integrity gate fails closed on empty / mode-mismatched output (prevents Release named `main` from a github.ref fallback).
• Annotation-injection sanitization on every logged ref.
• Explicit `secrets:` passthrough on docker.yml (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN); ci.yml no longer inherits anything.
• `if: failure()` cleanup auto-deletes v-tag + rc-marker on partial failure (eliminates the external-consumer phantom-version ingestion window).
• ACTIONS_STEP_DEBUG window closed via `set +x` wrap on the inline auth-header compute.
• Curated retry-loud error handling on `gh api` bot-user-id lookup and `npx semver`.
Pre-merge validation:
• 10-reviewer multi-agent code-review pass; 14 findings fixed inline (commit 820cefae), 6 deferred to follow-ups.
• End-to-end dry-run rehearsal via workflow_dispatch (run 25919563064) validated route classification, rc-guard, App token mint, RC checkout, version resolver, vtag synthetic-regex check, and faithful tarball pack at the bumped version.
• All zizmor findings on the unification commits closed.
• Branch-protection required checks all green.
Post-merge actions:
• After the first successful RC, delete the `NPM_TOKEN` and `RELEASE_PUSH_TOKEN` secrets — they are no longer used.
• The first real RC after merge is the live-fire test for steps dry-run could not exercise (atomic tag push, real npm OIDC handshake, GitHub Release creation, docker.yml under explicit secrets passthrough). The if: failure() cleanup step handles the partial-failure recovery automatically; the Rollback Runbook in CONTRIBUTING.md covers the rare cases auto-cleanup can't reach.
* Initial plan
* Merge C++ ADL and ordinary free-call candidate sets
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1aea3511-3471-4ec2-9819-0fb27ac40b89
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* Address review feedback on merged ADL ambiguity suppression
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1aea3511-3471-4ec2-9819-0fb27ac40b89
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: apply prettier to C++ ADL resolver fallback files
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9b9c1494-bc69-4db5-a89d-69eb816bab82
* docs: update ADL ambiguity comments to merged narrowing flow
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9b9c1494-bc69-4db5-a89d-69eb816bab82
* fix: suppress global fallback when merged ADL narrowing yields zero candidates
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9b9c1494-bc69-4db5-a89d-69eb816bab82
* docs: clarify free-call fallback comment for ADL merged path
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9b9c1494-bc69-4db5-a89d-69eb816bab82
* feat: ADL Gap 2 — enum-typed arguments contribute enclosing namespace
ISO C++ [basic.lookup.argdep] §2: "If T is an enumeration type, its
associated namespace is the namespace in which it is defined."
- Add Enum to findCppClassDefBySimpleName type filter
- Map Enum defs to enclosing namespace in populateCppAssociatedNamespaces
- Add test fixture cpp-adl-enum-arg with color::Channel enum
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ca8987b6-365e-4034-af56-ca3f9b439902
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat: ADL Gap 6 — inline namespace expansion in associated set
ISO C++ inline namespaces are transparent for ADL: if a namespace is
in the associated set, candidates declared in its inline-namespace
children are also reachable.
- Expand pickCppAdlCandidates to scan inline-namespace children of
associated namespaces (via isCppInlineNamespaceScope predicate)
- Add test fixture cpp-adl-inline-ns-expansion: Event in outer audit,
record in inline v1, other::record(int) forces arity disambiguation
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ca8987b6-365e-4034-af56-ca3f9b439902
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat: ADL Gap 1 — hidden friend functions visible via ADL
ISO C++ [basic.lookup.argdep] §2: friend functions declared inside a
class body are visible via ADL when the class is an associated class.
- Exempt friend_declaration from cppLabelOverride's class-body function
suppression (c-cpp.ts) so friend function defs are captured
- Scan Function scopes that are direct children of associated Class
scopes in pickCppAdlCandidates (adl.ts) to find hidden friends
- Add test fixture cpp-adl-hidden-friend: `friend void process(Foo&)`
declared inside lib::Foo, resolved via ADL from app::run()
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ca8987b6-365e-4034-af56-ca3f9b439902
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat: ADL Gap 3 — non-function ordinary lookup suppresses ADL
ISO C++ [basic.lookup.unqual] §7: if ordinary unqualified lookup finds
a name that is not a function or function template, ADL is not performed.
- Add hasNonCallableBindingInScope walker in walkers.ts
- In free-call-fallback, check for non-callable binding before invoking
ADL; when found, bypass resolveAdlCandidates entirely
- Add test fixture cpp-adl-non-function-blocks: variable `int record`
shadows the function name, blocking ADL from finding audit::record
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/ca8987b6-365e-4034-af56-ca3f9b439902
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: use nearest-scope semantics for ADL non-callable blocker check
Finding 1: `hasNonCallableBindingInScope` walked the entire scope chain,
which could incorrectly suppress ADL when an inner scope had a callable
and an outer scope had a non-callable for the same name. Per ISO C++
`[basic.lookup.unqual]` §7, ADL is blocked only when ordinary lookup
itself finds a non-function — if ordinary lookup stops at an inner scope
where only callables exist, ADL should still fire.
Replace the separate `hasNonCallableBindingInScope` + `findAllCallable
BindingsInScope` calls with a combined `findCallableBindingsAndAdlBlocker`
walker that stops at the first scope with ANY binding for the name and
returns both `{ callables, nonCallableFound }`. One pass, one stop.
Fixture: cpp-adl-inner-callable-outer-noncallable — inner scope has
callable `swap(int,int)`, outer scope has `int swap = 0`. ADL fires and
resolves to `data::swap(Pair&,Pair&)` via argTypes narrowing.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2f97daf-17fd-4891-8b10-a81e44d32808
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix: block-scope function declaration suppresses ADL
Finding 2: ISO C++ [basic.lookup.argdep] lists three ADL blockers:
1. class member declaration (handled by pickImplicitThisOverload)
2. block-scope function declaration NOT a using-declaration (NEW)
3. non-function/non-template declaration (handled by nonCallableFound)
Extend `findCallableBindingsAndAdlBlocker` to return `blockScopeDeclFound`
when a callable is found at a Function or Block scope — indicating a local
forward declaration that should suppress ADL per standard.
`free-call-fallback.ts` now checks both `nonCallableFound` and
`blockScopeDeclFound` to determine ADL suppression.
Fixture: cpp-adl-block-scope-decl-blocks — `void record(int);` declared
inside function body prevents ADL from discovering audit::record.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2f97daf-17fd-4891-8b10-a81e44d32808
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* docs: update stale ADL_AMBIGUOUS comment in unqualified-ref-collision fixture
Finding 3: The `ADL_AMBIGUOUS` sentinel was removed by this PR (replaced
by `isOverloadAmbiguousAfterNormalization` in merged-narrowing). Update
the fixture comment to reference the current mechanism.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2f97daf-17fd-4891-8b10-a81e44d32808
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test: add legacy-parity expected failures for ADL blocker tests
The new ADL nearest-scope blocker and block-scope function declaration
tests rely on scope-resolution-only mechanisms not present in the legacy
DAG path. Register them in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2f97daf-17fd-4891-8b10-a81e44d32808
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* chore: revert unrelated prettier-plugin-tailwindcss devDep addition
The `prettier-plugin-tailwindcss` dependency was accidentally added while
running local prettier; it is not needed for the C++ ADL changes.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a2f97daf-17fd-4891-8b10-a81e44d32808
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* Initial plan
* fix(cpp): include base-class namespaces in ADL candidate selection
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7df6f692-1af9-43e6-82de-099ed43a60cb
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): clarify ADL base-namespace test names
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7df6f692-1af9-43e6-82de-099ed43a60cb
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): remove stale legacy parity expected-failure entry
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1a55a5e8-ae91-44bc-9b21-9324cdfea3de
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): assert base-namespace ADL tests are not parity skips
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b3726b70-e797-4f37-955d-7d61fd28d338
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(cpp): avoid MRO amplification on ambiguous class-name ADL lookup
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b3726b70-e797-4f37-955d-7d61fd28d338
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): strengthen ADL base-namespace target identity assertions
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b3726b70-e797-4f37-955d-7d61fd28d338
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): add ADL negative cases for anonymous and unresolved bases
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b3726b70-e797-4f37-955d-7d61fd28d338
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(cpp): fix anonymous-base parity expectation and formatting
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6a2e3cf9-beea-435c-8494-6a7a00af0f1e
* fix(cpp): propagate unnamed-namespace members through #include in registry-primary resolver
Anonymous-namespace contents in a header (e.g. `namespace { void f(); }`)
are reachable by unqualified lookup in any TU that #includes the header
per ISO C++ [basic.namespace.anon]/1 (the unnamed namespace behaves as
if a `using namespace unique;` is inserted into the enclosing scope, with
per-TU `unique`). The registry-primary path was filtering these defs out
of `expandCppWildcardNames` via both the structural Namespace-owner check
and the `isFileLocal` mark, so `hidden_probe(d)` from a TU including the
header resolved to nothing while the legacy DAG returned the correct edge.
Track anonymous-`namespace_definition` source ranges at capture time,
resolve them to ScopeIds in `populateOwners` (parallels inline-namespace
handling), and exempt those scopes from the two wildcard-expansion filters
plus the `populateCppNonGloballyVisible` structural set. `markFileLocal`
is preserved so the global free-call fallback still blocks cross-TU leaks
for files that do NOT #include the declaring file (cpp-anon-ns-cross-file
guard still passes).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
* fix(cli): tolerate read-only workspace in ensureGitNexusIgnored
The documented Docker workflow mounts the host workspace at /workspace:ro
and runs `gitnexus index /workspace/<repo>` against an index produced by
a prior host-side `analyze`. Since PR #1248 ("keep GitNexus ignores
inside .gitnexus") the index command has called `ensureGitNexusIgnored`,
which unconditionally writes `<repo>/.gitnexus/.gitignore` and
`<repo>/.git/info/exclude` — both fail with EROFS on the :ro bind mount
even though the host already wrote the correct file during `analyze`.
Two complementary changes:
1. Idempotent fast path. Read the existing .gitnexus/.gitignore content
first; if it already matches the desired value (`*\n`), skip the
write entirely. This is the common case for the Docker workflow and
avoids touching the FS at all.
2. EROFS/EACCES tolerance. When a write is genuinely needed but the FS
refuses it, log a structured warning via the existing pino logger
and continue. `registerRepo` runs before `ensureGitNexusIgnored` in
`indexCommand`, so the global-registry write is already committed
when we get here — letting the gitignore-write failure propagate
leaves the user with a registered-but-error-exited command.
Three new unit tests pin the behaviour:
- idempotent re-call leaves mtime untouched
- ENOENT-then-correct path on a writable parent succeeds
- :ro parent (simulated via chmod 0o555) does not throw, on the
already-correct fast path and on the cold-create path
Existing tests (61) still pass.
Closes#1549.
* test(storage): cover read-only ignore paths and tolerate EPERM (#1550)
- Add isReadOnlyFilesystemError helper including EPERM alongside EROFS/EACCES
for ensureGitNexusIgnored and ensureGitInfoExclude (Windows parity with
lbug-config / bridge-db patterns).
- Skip chmod-based read-only tests on win32 and uid 0; assert logger.warn
on POSIX chmod denial for missing .gitignore.
- Add repo-manager-ensure-ignore-readonly.test.ts with vi.mock fs/promises
delegating writeFile so EROFS/EACCES/EPERM rejections are asserted with
structured log path and message for both .gitignore and .git/info/exclude.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(claude): skip augment hook when server owns db
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(hooks): cross-platform DB lock probe for MCP owner guard
Extract hook-db-lock-probe.cjs with a single hasGitNexusDbLockedByGitNexusServer
entry point used by both Claude hooks:
- Linux: scan /proc/<pid>/fd via dev+inode (no lsof required), optional lsof
fallback; GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS caps scan time
- macOS and other Unix: trusted lsof + ps (absolute paths / env overrides)
- Windows: Restart Manager + Win32_Process via win-rm-list-json.ps1 and
GITNEXUS_HOOK_POWERSHELL_PATH
Update hooks.test.ts source coverage for the probe module.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Update gitnexus/hooks/claude/win-rm-list-json.ps1
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Apply suggestion from @github-actions[bot]
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(gitnexus): repair package.json JSON after malformed engines edit
Co-authored-by: Cursor <cursoragent@cursor.com>
* Update Node.js engine version requirement to 22.0.0
* Update Node.js engine version to >=22.0.0
* fix(hooks): address ce-code-review findings on PR #1493
P0:
- Replace malformed `RM_UNIQUE_PROCESS` block in
`gitnexus/hooks/claude/win-rm-list-json.ps1` (duplicate struct decl +
duplicate `ProcessStartTime` + unbalanced braces) with a single
well-formed `[StructLayout(LayoutKind.Sequential, Pack = 4)]` struct,
so PowerShell `Add-Type` actually compiles and the Windows DB-lock
probe stops fail-open on every machine.
- `gitnexus/src/cli/setup.ts` now copies `hook-db-lock-probe.cjs` and
`win-rm-list-json.ps1` into the user's `~/.claude/hooks/gitnexus/`
alongside `hook-lock.cjs`, preventing the `MODULE_NOT_FOUND` thrown
by `gitnexus-hook.cjs:18`'s top-level require on every fresh install.
`gitnexus/test/unit/setup.test.ts` extended to assert both new copy
destinations.
- Four fail-open hook tests (`ENOENT lsof`, `npx parent line`,
`non-GitNexus ps line`, `ps ENOENT`) now seed `createHookToolDir`
with a valid `[GitNexus]` stderr line so
`expect(parseHookOutput).not.toBeNull()` actually holds on CI.
P1:
- Plugin copy of `win-rm-list-json.ps1` gains `Pack = 4` so its CLR
struct matches the 12-byte native `RM_UNIQUE_PROCESS` layout
(multi-blocker `RmGetList` no longer reads mangled `dwProcessId`).
- `GITNEXUS_HOOK_CLI_PATH = ''` now falls through to the resolution
chain in `gitnexus-hook.cjs`, matching the plugin copy and removing
the twin-file divergence on empty-string envs.
- Lock-warning suppression test seeds `gitnexusMarkerPath` and asserts
the augment subprocess actually ran, plus `GITNEXUS_DEBUG=1`
preserves the full discarded prefix.
- MCP-owner skip branch in both hook copies now emits
`[GitNexus] augment skipped: MCP server owns DB` on stderr, so
agents can distinguish intentional skip from silent failure.
P2:
- `ps` loop in `hook-db-lock-probe.cjs` fails-closed on `ETIMEDOUT`
to mirror the `lsof` handling (symmetric subprocess-probe contract).
- `RmStartSession` return value captured in both `.ps1` copies; exits
early with `[]` on non-zero so subsequent RM API calls don't operate
on an invalid handle.
- Windows RM-list `.ps1` encoded cache distinguishes uninitialized
(`undefined`) from load-failed (`null`) with a one-shot
`GITNEXUS_DEBUG` warning instead of silently caching empty string.
- `createHookToolDir` helper accepts `lsofOutputLines` and
`psOutputByPid`; the multi-PID test uses them instead of duplicating
the fake-binary construction inline.
- All five skip-path tests now assert `result.status === 0` and the
new skip-signal stderr line.
- `AGENTS.md` documents the seven hook configuration env vars
(`GITNEXUS_HOOK_CLI_PATH`, `_LSOF_PATH`, `_PS_PATH`,
`_POWERSHELL_PATH`, `_LINUX_PROC_BUDGET_MS`, `_RM_TARGET`,
`GITNEXUS_DEBUG`).
- `GITNEXUS_DEBUG` path in `gitnexus-hook.cjs`/`.js` writes the full
discarded stderr prefix instead of a 180-char preview.
- Inline comment in `hook-db-lock-probe.cjs` explains the intentional
Windows ETIMEDOUT fail-closed semantics.
- Removed the unnecessary `as WriteFileOptions` cast and orphaned
`import type { WriteFileOptions }` in `hooks.test.ts`.
P3:
- `isGitNexusServerCommand` unexported from
`hook-db-lock-probe.cjs` (kept as private helper).
- Env-path overrides (`GITNEXUS_HOOK_CLI_PATH`,
`_POWERSHELL_PATH`, `_LSOF_PATH`, `_PS_PATH`) require
`fs.existsSync` before being returned, so typos / stale config fall
through to the standard resolution chain.
Misc:
- `gitnexus/package.json` engines.node back to `>=22.0.0` (matches
origin/main and the original PR reviewer's earlier request).
Twin-tree parity / CI sync mechanism tracked separately at
abhigyanpatwari/GitNexus#1591.
Test plan: vitest run test/unit/hooks.test.ts → 113 passed,
18 Unix-only skipped; setup.test.ts → 14 passed.
* chore(autofix): apply prettier + eslint fixes via /autofix command
* trigger
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: apply ESM .js extension fallback to tsconfig path alias resolution
Path alias imports (e.g. `@/utils.js` via tsconfig paths) now correctly
strip JS-family extensions and retry with TS equivalents when the literal
.js file does not exist. This applies the same stripJsExtension fallback
already used for relative imports to the alias resolution branch.
Fixes#1528
* chore(autofix): apply prettier + eslint fixes via /autofix command
* test(esm): cover .mjs/.cjs path-alias extension resolution
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(esm): use Map for path aliases in resolveWithAlias helper
Matches TsconfigPaths.aliases from language-config. CI cannot run tsc -p tsconfig.test.json yet: the project has hundreds of pre-existing errors under test/ (fixtures + unit/integration); enable that step after backlog cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Initial plan
* fix(cpp): workspace-wide dependent-base name resolution (cross-file support)
- Replace per-file `populateCppDependentBases(parsed)` with a workspace-wide
`populateCppDependentBases(parsedFiles)` that builds a cross-file class index
- Use qualified-name prefix for namespace disambiguation when multiple classes
share a simple name (e.g. `Box` in two namespaces)
- Move the call from `populateOwners` (per-file) to the new `populateWorkspaceOwners`
hook so all files are processed before resolution runs
- Add `cpp-two-phase-dependent-base-ns` fixture: Base<T> in a namespace in a
separate file from Derived<T>, plus a namespace-free function with the same
name — exercises the path where the class-owned filter does not apply
- Add two integration tests for the new fixture"
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/d78fae8b-cd32-45d8-a815-2b27d7d89e62
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(cpp): clarify V1 conservative exact-prefix namespace match in two-phase-lookup
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/d78fae8b-cd32-45d8-a815-2b27d7d89e62
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* fix(cpp): complete scope-resolution parity
* fix(ci): resolve formatting, lint errors for PR #1520
- prettier: format arity-metadata.ts, captures.ts, index.ts
- eslint: rename unused HEADER_GLOB to _HEADER_GLOB
- eslint: replace unsafe parser.parse() with parseSourceSafe()
- eslint: suppress intentional console.warn/log in sync.ts
- eslint: remove unused _it import alias in cpp.test.ts
* fix(ci): complete formatting, lint, and typecheck fixes
- prettier: format call-processor.ts, imported-return-types.ts,
include-extractor.test.ts, cpp-captures.test.ts, cpp-imports.test.ts
- eslint: suppress intentional console.warn in manifest-extractor.ts
- typecheck: restore 'thrift' in ContractType union (was accidentally
removed) and add thrift case to exhaustive switch in manifest-extractor
* fix(ci): revert unintended group module changes that broke tests
Restore types.ts, config-parser.ts, matching.ts, sync.ts, and
manifest-extractor.ts to upstream/main versions. The original commit
accidentally removed fields (thrift, workspace_deps, exclude_links_paths,
exclude_links_param_only_paths) from DetectConfig/MatchingConfig/ContractType
which are still referenced by matching.test.ts, config-parser.test.ts,
sync.test.ts and other integration tests.
This PR's scope is C++ scope-resolution parity only — group module
type definitions and logic should remain unchanged.
* fix(codeql): address security and quality alerts
- arity-metadata.ts, interpret.ts: replace single-pass template strip
regex (/<[^>]*>/g) with a while-loop to fully handle nested templates
like Map<List<int>> — resolves 'Incomplete multi-character sanitization'
- cpp.test.ts: remove unused vitest 'it' import since the file defines
its own 'it' via createResolverParityIt — resolves 'Assignment to constant'
- include-extractor.test.ts: use fs.mkdtempSync() instead of predictable
os.tmpdir()+Date.now() paths — resolves 'Insecure temporary file'
- interpret.ts: remove redundant 'name !== undefined' check (already
guaranteed by early return) — resolves 'Comparison between inconvertible types'
* review: address Claude review findings on PR #1520
- Findings 1-3 (BLOCKERS): restore include-extractor.ts and its test to
the main baseline. Block-comment fallback regression, suffix-resolve
false-positive suppression, and the four deleted regression tests
(#3-#6) are now back. These changes were unrelated to C++ scope
parity and should not have been in this PR.
- Finding 4 (MAJOR, partial): revert COMPOUND_RECEIVER_MAX_DEPTH 6 to
4. No C++ test exercises depth > 4 (cpp-chain-call uses a 2-hop
chain), so the bump risked silent regressions on other migrated
languages without justification. The wildcard-origin propagation in
imported-return-types.ts is retained — C++ #include and using
namespace both emit wildcard-origin bindings (cpp/import-decomposer
.ts:40,90), so wildcard propagation is causal to C++ parity.
- Finding 6: tighten write-access dedup test with exact per-field
counts (nameWrites = 2, addrWrites = 1) instead of total-count + sub
string containment, so a regression in one of the two name writes
can no longer be masked.
- Finding 8: skipped. Box-drawing characters in cpp/query.ts comments
match the established convention used in csharp/java/php query
files.
Finding 5 (int/long normalization tie-breaker) left as documented
follow-up — proper fix requires resolver-level tie-breaker logic and
risks regressing other arity-matching tests.
* fix(cpp): stop #include from leaking class methods and namespace members (U1)
The C++ registry-primary resolver was emitting impossible CALLS edges
for ordinary headers: an including file's unqualified save() resolved
to User::save and unqualified foo() resolved to ns::foo. Two leak
paths converged on localDefs:
1. expandCppWildcardNames (file-local-linkage.ts) iterated the
flattened localDefs and exported every simple tail, including
class-owned methods and namespace-contained symbols. Replaced with
a scope-aware filter: build nodeId -> owning Scope from
Scope.ownedDefs and skip defs whose owning scope is Namespace or
Class.
2. The shared global free-call fallback's pickUniqueGlobalCallable
walks the workspace registry by simple name and would still hit
class methods / namespace members even with wildcard expansion
fixed. Plugged the gap via the existing isFileLocalDef hook —
semantically 'logically invisible cross-file' — by tracking per-
file non-globally-visible nodeIds (populateCppNonGloballyVisible,
called from populateOwners) and adding an ownerId !== undefined
fast-path for class-owned defs.
Side fix in shared finalize-algorithm.ts: when wildcard expansion
resolves to a real target but produces zero propagating names, the
edge was dropped, taking the file-level IMPORTS edge with it.
Preserve the original wildcard edge so #include dependencies survive
even when the header exposes no unqualified bindings.
Tests: cpp-include-no-class-leak, cpp-include-no-namespace-leak, and
cpp-anon-ns-same-file-visible fixtures. Negative tests mode-gated to
REGISTRY_PRIMARY_CPP=1 via the expected-failures registry — legacy
DAG has no scope-aware filtering on the global fallback; backporting
is out of scope. All 2104 resolver integration tests pass under
registry-primary mode.
* fix(cpp): suppress receiver-bound CALLS when integer-width overloads collide (U2)
C++ arity-metadata normalizes int, long, short, unsigned, size_t to
'int' so single-candidate flows like 'process(42L)' match a 'long'-
typed parameter via loose matching. But when both 'process(int)' and
'process(long)' coexist as method overloads, they both end up with
parameterTypes=['int'] in the registry, and pickOverload's narrowing
returns 2 candidates with no way to disambiguate. The previous code
picked candidates[0] arbitrarily, emitting a CALLS edge to the wrong
overload roughly half the time.
Fix:
- Add isOverloadAmbiguousAfterNormalization in overload-narrowing.ts
that detects >1 candidate sharing identical parameterTypes sequences.
- Have pickOverload return a new OVERLOAD_AMBIGUOUS sentinel when this
fires.
- In the receiver-bound-calls loop, when pickOverload signals ambiguity,
suppress the edge AND add the site to handledSites so the late-stage
emitReferencesViaLookup pass does not re-emit the pre-resolved
reference. Without the handled-mark, the reference index still
carries a toDef and emits the same wrong edge.
Graph schema has no ambiguous-target edge model, so emitting two
edges (one per candidate) would require a separate schema change.
Zero-edge is the only safe outcome.
Other languages: the ambiguity check is a precondition gate, not a
behavior change for normal narrowing. Languages whose normalizers do
not collapse distinct types into a single token (verified by grep
over *-arity-metadata.ts) will never produce >1 candidate with
identical parameterTypes from genuinely distinct declarations, so
the branch is effectively C++-only in practice.
Test: cpp-overload-int-long fixture asserts exactly .toBe(0) CALLS
edges. Count=1 = arbitrary pick (the bug); count>1 = unsupported
ambiguous-edge model. Mode-gated to REGISTRY_PRIMARY_CPP=1 — legacy
DAG has no OVERLOAD_AMBIGUOUS wiring; backporting is out of scope.
All 2105 resolver integration tests pass under registry-primary; all
139 cpp tests pass under both modes (3 negative tests skipped in
legacy as documented).
* test(cpp): add integration coverage for anonymous-namespace, using-namespace conflict, and std-shim leakage (U3+U4+U5)
Three new end-to-end fixtures exercise the resolver pipeline against
scenarios that previously had only unit-level coverage or no coverage
at all (Claude review Finding 7):
U3 — cpp-anon-ns-cross-file:
helper.cpp declares 'namespace { void worker(); }' and calls it
internally. caller.cpp declares a separate 'void worker()' and calls
it. Asserts (a) the cross-file CALLS edge from caller's run() does
not target helper.cpp's anonymous-namespace worker, and (b) the
same-file edge from helper_entry() to its own worker still resolves
(positive guard against a 'no edges at all' regression making the
negative check vacuously pass). Includes a state-isolation guard
that re-runs the same fixture and asserts identical results,
proving clearFileLocalNames() is called by the pipeline entry.
U4 — cpp-using-namespace-conflict:
Two headers each declaring 'namespace a { foo() }' and
'namespace b { foo() }' respectively, plus a caller doing
'using namespace a; using namespace b; foo()'. Asserts exactly
zero CALLS edges. One edge = arbitrary pick (the bug); two edges
would require an ambiguous-target edge model GitNexus does not
have. Depends on U1 — without scope-aware filtering, both foo()s
would already be in the importer's wildcard binding set as simple
'foo', so the test would pass for the wrong reason.
U5 — cpp-using-namespace-std-smoke:
Fixture-local 'namespace std { void cout_write(); void println(); }'
shim rather than real <iostream> — captures the wildcard-leak
shape deterministically without depending on system-header modeling
stability (out of scope per plan). Asserts (a) the project-local
call resolves correctly, (b) no leak to shim STL symbols, and (c)
no CALLS/ACCESSES edges from the caller into std-shim.h at all.
Negative tests for U2/U4 mode-gated to REGISTRY_PRIMARY_CPP=1 via
the expected-failures registry; legacy DAG lacks the OVERLOAD_AMBIGUOUS
suppression and the namespace-aware filtering, so the leaks persist
there. All 2112 resolver integration tests pass under registry-primary;
all 146 cpp tests pass under both modes (4 negative tests skipped in
legacy as documented).
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(cpp): scope-aware isSuperReceiver classification (U1)
The C++ isSuperReceiver hook used a regex `/^[A-Z]\w*::/` that
misclassified any uppercase-qualified call as a super-receiver call.
Singleton::getInstance(), std::Foo::bar(), and PascalCase namespace
calls all entered the super branch, where the absence of an enclosing
class (or wrong MRO context) dropped the resolution entirely.
Fix:
- New optional ScopeResolver hook isSuperReceiverInContext(text,
callerScope, scopes). Languages where super classification depends
on caller context define it; receiver-bound-calls.ts prefers it
when defined and falls back to the simple isSuperReceiver(text)
otherwise. Other migrated languages (Python, Java, C#, PHP, Go,
TypeScript) are unchanged.
- C++ implementation: parse the LHS of '::' from the receiver text,
resolve via findClassBindingInScope, and return true only when
the LHS is a class-like def in the caller's enclosing class's MRO.
Returns false for namespace LHS, unresolved LHS, self-class LHS
(qualified self-calls aren't super), and any non-'::' form.
- Extended the C++ tree-sitter query to capture the LHS of
qualified_identifier as @reference.receiver so qualified static
member calls (Singleton::getInstance()) reach the receiver-bound
Case 2 (class-name receiver) path. Without the receiver capture,
qualified calls had no explicit receiver and could not resolve
through any receiver-bound branch.
Test: cpp-namespace-qualified-not-super fixture. Singleton::getInstance()
from a free function asserts exactly 1 CALLS edge through the
qualified-call path. Passes under both REGISTRY_PRIMARY_CPP=1 and =0.
All 2113 resolver integration tests pass; all 147 cpp tests pass under
both modes.
* fix(cpp): suppress receiver-bound CALLS when default-arg overloads collide (U4)
ISO C++ rejects 's.f(1)' as ambiguous when both 'void f(int)' and
'void f(int, int = 0)' are declared on S. The previous resolver
returned the first viable candidate via pickOverload's fallback.
Extended isOverloadAmbiguousAfterNormalization to take an optional
argCount: when provided, the predicate compares only the first
argCount slots of each candidate's parameterTypes. Candidates whose
declared-prefix matches up to argCount are treated as ambiguous
because default arguments make all of them equally viable for the
call.
Without argCount, behavior is unchanged (the original int/long
normalization-collapse contract, full-length equality required).
pickOverload now passes site.arity so default-arg ambiguity fires.
Test: cpp-overload-default-arg-ambiguous fixture. s.f(1) where S has
f(int) and f(int, int = 0) asserts exactly .toBe(0) CALLS edges.
Passes under both REGISTRY_PRIMARY_CPP=1 and =0.
All 2114 resolver integration tests pass; all 148 cpp tests pass
under both modes.
* fix(cpp): two-phase template lookup suppresses dependent-base members (U3)
ISO C++ two-phase name lookup: inside a class template body, unqualified
calls MUST NOT bind to members of a dependent base class. Only this->name
or Base<T>::name forms make the lookup dependent. GCC and Clang both
reject the unqualified form with 'declaration of f must be available'.
Before this fix, GitNexus's global free-call fallback walked the
workspace registry by simple name and bound unqualified calls inside
template bodies to dependent-base members, producing CALLS edges the
compiler would reject.
Implementation:
- New languages/cpp/two-phase-lookup.ts module: per-pipeline state
recording (className, dependentBaseName) pairs at capture time and
resolving them to nodeId sets during populateOwners.
- captures.ts detectCppDependentBases walks the AST once finding every
template_declaration containing a class/struct definition. For each,
it collects template-parameter names (typename T, class T, non-type
int N, template-template parameters) and walks each base in the
base_class_clause checking whether any inner type_identifier matches
a template parameter. Conservative bias: typename T::U, decltype,
and template-template-parameter shapes also classified as dependent.
- Extended scope-resolution contract's isCallableVisibleFromCaller
hook with optional callerScope and scopes fields. C++ implements
the hook to consult isCppDependentBaseMember: when the candidate
is a member of a dependent base of the caller's enclosing class,
the hook returns false and pickUniqueGlobalCallable skips the
candidate.
- clearFileLocalNames also clears the dependent-base state per
pipeline run.
Fixtures:
- cpp-two-phase-dependent-base: Derived<T> deriving from Base<T>,
unqualified f() and i inside Derived's body. Asserts zero CALLS
edges and zero ACCESSES edges respectively.
- cpp-two-phase-this-qualified, cpp-two-phase-non-dependent-base,
cpp-two-phase-namespace-free-call-inside-template: positive
fixtures left as documented gaps (this-> and qualified-name
resolution inside template bodies are pre-existing resolver
weaknesses independent of U3). Tracked separately.
Negative test mode-gated to REGISTRY_PRIMARY_CPP=1 via the expected-
failures registry; legacy DAG has no two-phase lookup.
All 2116 resolver integration tests pass under registry-primary; all
150 cpp tests pass under both modes (5 negative tests skipped in legacy
as documented).
* fix(cpp): implement V1 ADL (Koenig lookup) for free-function calls (U2)
Plan 2026-05-13-001 U2. Adds argument-dependent lookup as a new
candidate-generating tier in `emitFreeCallFallback`: when ordinary
unqualified lookup is empty, ADL surfaces candidates from each
value-class-typed argument's enclosing namespace.
V1 boundary (locked by cpp-adl-pointer-arg-boundary fixture):
- only direct enclosing-namespace closure
- only directly-named class-type values (pointer / reference / template-
spec args excluded; closure rules deferred to V2)
- ADL fires ONLY when ordinary lookup is empty (no union-and-resolve)
Parenthesized name `(f)(s)` suppresses ADL per ISO C++
[basic.lookup.argdep]/3.1. Multi-candidate ambiguity (e.g. `process(int)`
vs `process(long)` after C++ int-width normalization) returns the
ADL_AMBIGUOUS sentinel — caller suppresses entirely, mirroring the
OVERLOAD_AMBIGUOUS contract from plan 2026-05-12-002 U2.
Implementation:
- `cpp/adl.ts` — new module: per-pipeline argInfoBySite + noAdlSites Maps
populated at capture time, classToNamespaceQualifiedName Map populated
during populateOwners; `pickCppAdlCandidates` returns
SymbolDefinition | ADL_AMBIGUOUS | undefined
- `scope-resolution/contract/scope-resolver.ts` — adds optional
`resolveAdlCandidates` hook
- `scope-resolution/passes/free-call-fallback.ts` — invokes ADL hook
between `findCallableBindingInScope` and `pickUniqueGlobalCallable`;
marks site handled on `'ambiguous'` so emit-references doesn't retry
- `cpp/captures.ts` — detects `parenthesized_expression` function wrap;
per-arg classification (pointer/reference/value class) preserving the
shape info the existing arity-narrowing normalizer strips
- `cpp/scope-resolver.ts` — registers hook, populates associated
namespaces, clears state in loadResolutionConfig
Negative tests (parens, pointer-boundary, ambiguous) gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG has no V1/V2
ADL boundary or ADL_AMBIGUOUS suppression.
154/154 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
147 pass + 7 skipped under =0 (legacy parity baseline).
* fix(cpp): inline namespace transitive walking + qualified namespace resolution (U5)
Plan 2026-05-13-001 U5. Two ISO C++ inline-namespace semantics:
1. Unqualified-lookup transitive visibility: inline-namespace members
reach the enclosing namespace's scope as if declared there. The
`populateCppNonGloballyVisible` exemption keeps them globally visible
so cross-file unqualified lookup finds them.
2. Qualified-receiver transitive visibility: `outer::foo()` resolves to
`outer::v1::foo()` when `v1` is inline (and through arbitrarily-deep
nesting like `outer::v1::experimental::foo`, matching libc++ `__1` /
libstdc++ `__cxx11`).
The second behavior required a new resolver case in
`receiver-bound-calls.ts` (Case 1.5: language-specific qualified-receiver
member lookup) because C++ qualified-namespace member calls had no prior
resolution path — receiver-bound Case 1 only handled
`ParsedImport.kind === 'namespace'` (Python/JS-style) and Case 2 handles
class receivers, neither of which fired for `outer::foo()`. The new
hook `resolveQualifiedReceiverMember` is opt-in; languages without
C++-style qualified-name semantics omit it.
Implementation:
- `cpp/inline-namespaces.ts` — new module: per-pipeline
`inlineNamespaceRangesByFile` + `inlineNamespaceScopeIds` Sets;
`markCppInlineNamespaceRange` at capture time;
`populateCppInlineNamespaceScopes` resolves ranges → scope IDs;
`resolveCppQualifiedNamespaceMember` walks namespace scopes by simple
name and descends transitively through inline children only.
- `scope-resolution/contract/scope-resolver.ts` — adds optional
`resolveQualifiedReceiverMember` hook to the contract.
- `scope-resolution/passes/receiver-bound-calls.ts` — Case 1.5 invokes
the hook between Case 1 (namespace imports) and Case 2 (class-name
receiver). Returns undefined for non-namespace receivers so Case 2
still resolves class-qualified calls.
- `cpp/captures.ts` — detects `inline` keyword child on
`namespace_definition`; records 1-based range to match Scope.range.
- `cpp/file-local-linkage.ts` — `populateCppNonGloballyVisible` exempts
inline-namespace scopes so cross-file unqualified lookup keeps their
members visible.
- `cpp/scope-resolver.ts` — wires `populateCppInlineNamespaceScopes`
into populateOwners (BEFORE `populateCppNonGloballyVisible` so the
exemption sees populated state); registers
`resolveQualifiedReceiverMember` hook.
4 fixtures: `cpp-inline-namespace-unqualified`, `-versioned`,
`-nested` (two transitive inline hops, STL `__1` shape), and
`-adl-participation` (composes with U2 — ADL surfaces records declared
inside inline child namespaces). All 4 assert exactly 1 CALLS edge with
correct target file.
Versioned fixture gated under LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp
— legacy DAG can't disambiguate two same-name foos without inline
awareness. Other 3 coincidentally resolve in legacy.
158/158 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
150 pass + 8 skipped under =0 (legacy parity baseline).
* test(cpp): Phase 5 cross-unit composition tests for U1/U2/U3/U5
Plan 2026-05-13-001 Phase 5. Locks in correct behavior at the
intersections between the previously-shipped scope-resolver units.
Enhancement to U1: `isSuperReceiverInContext` strips template-argument
lists (`Base<T>` → `Base`) and namespace prefixes (`outer::v1::Base` →
`Base`) before resolving the receiver in the caller's scope chain. This
makes the super-receiver classification work for template-class
heritage shapes like `Base<T>::method()` and `outer::v1::Base<T>::f()`.
Three fixtures + four tests:
- `cpp-phase5-u1-u3-qualified-base-call`:
`template<class T> struct Derived : Base<T>` with
`Base<T>::method()` inside a template body. Asserts NO mis-routing
(count = 0) — documents the V1 gap that template-class inheritance
isn't captured as EXTENDS by the legacy DAG, so MRO walks are empty
and the super branch can't dispatch. The composition still works
correctly: U1's template-arg-stripping classifies `Base<T>` as a
super candidate, but the empty-MRO terminates without false edges.
- `cpp-phase5-u2-u3-adl-from-derived`:
`Derived : Base<T>` where `Base::record` shadows `audit::record`.
Unqualified `record(e)` inside the template body should resolve via
ADL to `audit::record` (because U3 + the `isFileLocalDef` class-
owned filter suppress `Base::record`). Asserts 1 edge to audit.h
and 0 edges to base.h.
- `cpp-phase5-u3-u5-inline-base`:
`template<class T> struct Derived : outer::v1::Base<T>` where `v1`
is inline. Unqualified `f()` inside `Derived<T>::g()` should NOT
bind to Base::f (dependent-base suppression even across inline
namespace prefix). Asserts count = 0.
Phase 5 tests asserting no-false-positives are gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG over-
resolves without the template-arg-stripping qualified-receiver path
and without two-phase dependent-base suppression.
162/162 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
152 pass + 10 skipped under =0 (legacy parity baseline).
---------
Co-authored-by: HuangWenjie <zhoudeng.hwj@alibaba-inc.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(markdown): handle CRLF line endings in section heading parser
split('\n') on CRLF content leaves a trailing \r on each line, and the
heading regex /^(#{1,6})\s+(.+)$/ (anchored with $) fails to match
'## Heading\r' because $ matches before end-of-string, not before \r.
Result: Windows-authored markdown silently produces zero Section nodes.
Use split(/\r\n|\r|\n/) to normalize all line-ending conventions.
Pure additive — LF-only files produce identical output. CR-only (Mac OS
Classic) becomes tolerated as a side benefit at zero risk.
Adds integration test markdown-processor-crlf.test.ts covering LF
baseline, CRLF (the regression), CR-only, mixed, and startLine/endLine
correctness.
* test(markdown): strengthen CRLF integration tests + clarify split comment
- Assert section names, levels, line spans, and CONTAINS hierarchy (not only counts)
- Document trailing-newline effect on endLine via exact toEqual expectations
- Reword markdown-processor comment: \$ only at end-of-string vs .+ before \\r
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: empty commit
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): make --no-stats actually omit volatile counts (#1477)
Closes#1477.
The `--no-stats` flag on `gitnexus analyze` was advertised as
"Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md"
but had no effect: every reindex still rewrote the markdown with
fresh count phrases, producing chore-commit churn on every run —
the exact problem the flag was added to solve in #704.
Root cause is commander.js negation-flag semantics. `.option(
'--no-stats', ...)` registers the option under the accessor
`stats` (boolean, default `true`; `false` when the flag is passed),
NOT `noStats`. The two action-handler reads in `analyze.ts`
(lines 414 and 500 pre-fix) read `options?.noStats`, which is
always `undefined`, so the `noStats` payload always reached
`runFullAnalysis` / `generateAIContextFiles` as `undefined`/falsy
and the count branch in the template always fired.
Fixed by replacing `options?.noStats` with `options?.stats === false`
at both reads. The strict `=== false` check (rather than
`!options?.stats`) means absent options or absent `.stats` field
fall through as no-stats=false, preserving the documented default-on
behaviour. Also updated the `AnalyzeOptions` interface to declare
`stats?: boolean` (matching commander's actual output) with a
JSDoc explaining the negation, since the prior `noStats?: boolean`
shape was a static-type misrepresentation of what commander
provides at runtime.
Internal call sites that re-pack `{ noStats: ... }` for
downstream consumers (`run-analyze.ts`, `ai-context.ts`) keep
their existing field name — those interfaces are not commander-
shaped, so `noStats` is the correct name there.
## Regression tests
Two new unit tests in `test/unit/ai-context.test.ts`:
* `omits volatile counts when noStats option is set (#1477)` —
asserts the count parenthetical is absent from both CLAUDE.md
and AGENTS.md when `noStats: true` is passed.
* `preserves volatile counts when noStats is not set (default)` —
documents the default-on path so a future refactor can't
silently flip the default.
Both call `generateAIContextFiles` directly with distinctive numbers
that would unmistakably leak through if the omit branch is broken.
## Manual verification
* `vitest run test/unit/ai-context.test.ts` → 13/13 pass
(11 prior + 2 new).
* Verified before-fix behaviour by checking out main, running
`npx gitnexus analyze --no-stats` against an indexed repo, and
observing the count phrase still present. Re-running on the fix
branch with the same flag strips the phrase as documented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(cli): resolve merge conflict markers in analyze.ts (PR #1478)
Remove leftover conflict hunks from main merge; keep commander stats
shape (stats?: boolean), wire noStats: options?.stats === false into
runFullAnalysis and generateAIContextFiles, and retain indexOnly /
skipSkills / skipAgentsMd wiring from main.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): cover analyzeCommand → runFullAnalysis noStats bridge (#1477)
Assert commander-shaped options.stats maps to the internal noStats
payload (including explicit true/false and skipAgentsMd combination)
so the CLI bridge cannot regress without failing tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cli): cover AGENTS.md default stats + skills noStats bridge (#1478)
- Assert volatile stats phrase in both CLAUDE.md and AGENTS.md when noStats is omitted
- Add bridge test for --skills regeneration path with stats:false → generateAIContextFiles noStats
- Note shared noStats expression beside skills-path call; stub process.exit for full analyze path
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: gitnexus:keep marker preserves custom context sections
When <!-- gitnexus:keep --> is present inside the gitnexus block,
analyze only updates the stats line instead of replacing the entire
section with the verbose template. Lets users maintain lean custom
context without it being overwritten on every reindex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: improve gitnexus:keep marker to reliably preserve custom sections
The `<!-- gitnexus:keep -->` marker inside a GitNexus block tells
`analyze` to only update the stats line (node/edge/flow counts)
while preserving the user's custom layout. This lets teams trim
the verbose default template to a lean format without having it
overwritten on every reindex.
Changes:
- Broaden stats-line regex to match both "Indexed as" and
"indexed by GitNexus as" formats
- Improve stats extraction from generated content (prefer
structured match over greedy parentheses)
- If keep marker is present but no stats line found, preserve
the section as-is instead of falling through to full replace
- Add tests for keep preservation and no-keep replacement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR #1508 review findings (F1-F5)
Refactor the keep-marker stats-update path and close the test-coverage
gaps surfaced by the production-readiness review.
## Findings 2 + 3 (high) — fragile extraction → silent corruption
Stop re-extracting `newName` (first `**bold**`) and `newStats` (first
`(...)`, with fallback) from generated content. Both are structurally
fragile:
- F2: newName silently picks the wrong value if the template ever
emits bold text before the project-name line (no current bug; an
unstated contract with no enforcement)
- F3: newStats fallback `\(([^)]+)\)` matches `({target: "symbolName",
direction: "upstream"})` from the Always-Do bullet when
`noStats: true` suppresses the canonical stats line, silently
corrupting the stats output
Fix: pass `projectName: string` and `stats: RepoStats` directly into
`upsertGitNexusSection`. Build the stats line from those values. Both
callers in `generateAIContextFiles` already have them in scope.
## Finding 1 (high) — misleading return value
When a keep marker is present but no stats line matches the pattern,
the function previously returned `'updated'` without writing,
producing `CLAUDE.md (updated)` in CLI output for a file that was
not touched. Add a distinct `'preserved'` return variant; CLI now
reports `CLAUDE.md (preserved)` honestly.
## Finding 4 (medium) — unanchored stats regex
`/(?:Indexed as|...) \*\*[^*]+\*\* \([^)]+\)/` could match prose
embedded mid-paragraph in user content (e.g. "you'll see it Indexed
as **Foo** (note: ...)"). Anchor with `^...$` plus the `m` flag so
only standalone stats lines match.
## Finding 5 — test coverage gaps
Seven new tests, each cross-referenced to the review finding:
- keep marker OUTSIDE the GitNexus section has no effect
- AGENTS.md keep path preserves custom layout (parity with CLAUDE.md)
- idempotent: second run produces byte-identical output
- CRLF file with keep marker: stats line updates correctly
- noStats + keep marker: not corrupted by Always-Do tuple text (F3 regression guard)
- returns 'preserved' (not 'updated') when no stats line matches (F1 regression guard)
- project name with markdown punctuation (hyphens/slash/dot) lands intact
All 23 ai-context tests pass; typecheck, prettier, eslint clean.
* docs(ai-context): address PR #1508 review findings on keep-marker path
- Clarify that noStats affects generated template only, not keep-section stats updates
- Fix stats-line regex comment to match behavior (no end anchor; trailing suffix kept)
- Assert '. MCP tools.' survives stats replacement in preserve-custom-section test
- Document LF normalization when rewriting CRLF seed in keep-marker CRLF test
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: dp-web4 <dp@web4.ai>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat:(wiki) added --timeout and --retries flags for large module pages to mitigate timeout aborts
* docs(wiki): document --timeout and --retries options
* docs(wiki): document --timeout and --retries in SKILL.md
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
The README documents the Docker workflow as:
WORKSPACE_DIR=$HOME/code docker compose up -d
docker compose exec gitnexus-server gitnexus index /workspace/my-repo
…but `gitnexus` is not on $PATH inside the published image:
$ docker compose exec gitnexus-server which gitnexus
(empty)
$ docker compose exec gitnexus-server gitnexus --version
exec: "gitnexus": executable file not found in $PATH
The package.json `bin` entry (`"gitnexus": "dist/cli/index.js"`) would
normally surface via `node_modules/.bin/gitnexus`, but `npm prune
--omit=dev` in the builder stage strips that directory before the runtime
stage copies it in. The `dist/cli/index.js` itself already has the
`#!/usr/bin/env node` shebang and 755 permissions, so a single symlink
into /usr/local/bin makes the README's literal command work.
Verified locally:
$ docker build -f Dockerfile.cli -t gitnexus:local-pr-test .
$ docker run --rm gitnexus:local-pr-test gitnexus --version
1.6.4
$ docker run --rm gitnexus:local-pr-test gitnexus --help
Usage: gitnexus [options] [command]
…
$ docker run --rm -d --name t gitnexus:local-pr-test \
&& sleep 4 && docker exec t curl -s localhost:4747/api/health
{"status":"ok"}
CMD continues to invoke `node gitnexus/dist/cli/index.js serve …`
unchanged, so the change is additive and the server boot path is
untouched.
Refs #1549.