Skip to content

feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry#955

Merged
magyargergo merged 6 commits into
abhigyanpatwari:mainfrom
azizur100389:feat/analyze-name-alias
Apr 19, 2026
Merged

feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry#955
magyargergo merged 6 commits into
abhigyanpatwari:mainfrom
azizur100389:feat/analyze-name-alias

Conversation

@azizur100389

Copy link
Copy Markdown
Contributor

Closes #829.

Problem

The global registry at `~/.gitnexus/registry.json` keys indexed repos by `name` derived from `path.basename()`. Two projects whose top-level folders share a basename (e.g. both have an `app/` dir) both get `"name": "app"`, which makes:

  • `-r app` on `impact` / `context` / `query` ambiguous — picks whichever the resolver hits first.
  • The `Available: app, backend, website` error hint actively misleading — points at a disambiguator that doesn't disambiguate.
  • `gitnexus list` output identical for two different repos.

Reproduction from issue #829:

```bash
mkdir -p /tmp/gnx-a/app /tmp/gnx-b/app
cd /tmp/gnx-a/app && git init -q && echo "export const x = 1" > index.ts && git add . && git commit -qm init
npx gitnexus analyze
cd /tmp/gnx-b/app && git init -q && echo "export const y = 2" > index.ts && git add . && git commit -qm init
npx gitnexus analyze

npx gitnexus list # Both entries show as name: "app"
npx gitnexus impact x -r app # Which "app" does this target?
```

What's added

`analyze --name ` — override the registry `name` at index time. Persisted. Survives subsequent re-analyses of the same path without `--name` (preserved-alias precedence).

Duplicate-name guard — if another path already uses the requested name, `analyze` aborts with a `RegistryNameCollisionError` that names the conflicting path. The guard only fires when the user explicitly asks for a name that's taken (via `--name` or a preserved alias). Basename collisions still register silently so users unaware of `--name` see no behaviour change.

`--force` — allows intentional same-name coexistence. The colliding entries get disambiguated in list output + resolver errors (below) rather than at the storage layer.

Collision-aware error messages in `resolveRepo` (both "not-found" and "multiple-repos-no-param" branches). Two entries named `app` now produce:

```
Available: app (/tmp/gnx-a/app), app (/tmp/gnx-b/app), backend
```

instead of the pre-#829 misleading `Available: app, app, backend`.

Collision-aware `gitnexus list` output — colliding entries gain a `name (path)` header so they're visually distinct. Unique-name entries render byte-identically to pre-#829.

Item 3 from the issue — `-r` accepts absolute path

Already works in resolveRepoFromCache via `path.resolve(repoParam)` + exact match. This PR adds no new code for it, but the new tests exercise the case for regression coverage.

Ranking / precedence

When `registerRepo` resolves the final `name`:

  1. Explicit `opts.name` from `--name ` (highest priority).
  2. Preserved alias from an existing entry at the same path (if its stored `name` differs from its `path.basename` → it was aliased on a prior run).
  3. `path.basename(repoPath)` (the original default).

Behaviour decision flagged for reviewer

The issue's wording "Reject duplicate names on registration unless `--force` or `--name` is supplied" is ambiguous between:

  • Option A (strict) — reject ALL basename collisions without `--force`/`--name`. Would break existing users who don't know about the flag.
  • Option B (opt-in) — reject only when the user explicitly requests a name that's taken. Current implementation. Test highlight tool for agent, prompt enhancements #6 locks this in.

I went with Option B because Option A is a behaviour break. If you'd rather strict, it's a one-line conditional flip and one test update. Happy to follow reviewer preference.

Files changed

File Role
gitnexus/src/storage/repo-manager.ts `RegisterRepoOptions`, `RegistryNameCollisionError`, precedence + guard logic
gitnexus/src/cli/index.ts `--name ` option
gitnexus/src/cli/analyze.ts Thread `name` to `runFullAnalysis`; catch `RegistryNameCollisionError`
gitnexus/src/core/run-analyze.ts Forward `registryName` to `registerRepo`
gitnexus/src/cli/list.ts Collision-aware header format
gitnexus/src/mcp/local/local-backend.ts Collision-aware `Available: …` hint
gitnexus/test/unit/repo-manager.test.ts 6 new `it()` cases

Tests

6 new unit tests in `repo-manager.test.ts`, each isolating the registry via `GITNEXUS_HOME` pointing at a per-test tmpdir:

  1. `{ name: 'alias' }` stores the alias instead of basename.
  2. Re-register without `name` preserves an existing alias.
  3. Re-register with a different `name` overrides the previous alias.
  4. Collision with another path throws `RegistryNameCollisionError`, and the registry is not mutated by the failed call.
  5. `{ force: true }` allows the duplicate to coexist.
  6. Backward-compat — basename collisions without `--name` still register silently (confirms Option B).

Backward compat

  • Schema: unchanged. `registry.json` written by older versions loads unchanged; aliased entries look identical on disk to basename entries.
  • `registerRepo` signature: third parameter is optional; zero existing call sites need updating.
  • Error / list output: purely additive — colliding-name case gains a suffix; single-name case is byte-identical to pre-analyze has no way to disambiguate repos with the same basename — -r <name> becomes ambiguous #829.
  • Rollback: pure code revert, no on-disk state to unwind.

Scope declined

  • Strict-mode basename-collision rejection (Option A above) — reviewer's call.
  • `--as` as a second spelling of `--name` — issue mentions both, I ship just `--name`.
  • Hash-suffix list output instead of full path — full path is clearer for humans; hash is a trivial follow-up if terminal width becomes a concern.

Test plan

  • `npx vitest run test/unit/repo-manager.test.ts` → 15 pass (9 existing + 6 new)
  • `npx vitest run test/unit/calltool-dispatch.test.ts` → 65 pass (no regression on the modified resolver error path)
  • `npm run test:unit` → 3784 pass, 2 pre-existing env failures in `git-utils.test.ts` (Windows worktree tmpdir) unchanged
  • `npx tsc --noEmit` → clean
  • Pre-commit hook (lint-staged + eslint + prettier + tsc) → passed

… registry

The global registry at ~/.gitnexus/registry.json keys indexed repos by
`name`, derived from path.basename(). Two different projects whose
top-level folders share a basename (e.g. both have an `app/` dir) both
get `"name": "app"`, which makes:

  - `-r app` on impact/context/query ambiguous (picks whichever the
    resolver hits first)
  - the `Available: app, backend, website` error hint actively
    misleading
  - `gitnexus list` output identical for two different repos

Issue abhigyanpatwari#829 (from doc-l2) has clean reproduction steps and a four-item
wish list. This PR addresses the three-and-a-half items that have
meaningful code surface; item 3 (accept absolute paths on -r) was
already functionally supported in resolveRepoFromCache and is now
locked in by the new test coverage below.

Changes:

  * repo-manager.ts: extend registerRepo with optional
    `{ name, force }` options, a new RegistryNameCollisionError class,
    and a precedence rule:
      explicit --name > preserved existing alias > path.basename
    Preservation means re-running `analyze` on a path without --name
    keeps the alias set by an earlier --name run. The collision guard
    only fires when the caller EXPLICITLY asks for a name that's taken
    (via --name or a preserved alias) — basename collisions keep
    registering silently, preserving behaviour for users who don't know
    about --name yet.

  * cli/index.ts: add `--name <alias>` option to `analyze`.

  * cli/analyze.ts: thread `name` to runFullAnalysis as `registryName`,
    catch RegistryNameCollisionError with an actionable error message
    pointing at the conflicting path.

  * core/run-analyze.ts: extend AnalyzeOptions.registryName, forward
    to registerRepo({ name: registryName }).

  * cli/list.ts: collision-aware header format. Unique-name entries
    render identically; colliding entries gain ` (<path>)` suffix.

  * mcp/local/local-backend.ts: the `Available: …` hint in resolveRepo
    now annotates colliding names with their paths so the caller can
    actually pick the right one. Applied to both not-found and
    multiple-repos-no-param error branches.

Tests: 6 new it() blocks in test/unit/repo-manager.test.ts covering:
  1. { name: 'alias' } stores the alias instead of basename
  2. Re-register without name preserves an existing alias
  3. Re-register with a different name overrides the previous alias
  4. Collision with another path throws RegistryNameCollisionError
     and leaves the registry unmodified
  5. { force: true } allows the duplicate to coexist
  6. Backward-compat: basename collisions without --name still
     register silently (users unaware of --name see no break)

Each test isolates the registry via GITNEXUS_HOME pointing at a
per-test tmpdir, so tests don't touch the developer's real
~/.gitnexus/registry.json.

Backward compat:

  - registry.json schema unchanged. `name` is still a string; aliased
    entries look identical on disk to basename entries, only the
    precedence logic distinguishes them at runtime. Old registries
    load unchanged.
  - registerRepo third parameter is optional. Existing call sites
    (none outside run-analyze.ts) compile unchanged.
  - Error-message and list-output changes are additive — only the
    colliding-name case gains the path suffix, single-name cases are
    byte-identical to pre-abhigyanpatwari#829.
  - Users who never pass --name see zero behaviour change.

Scope declined for v1:

  - Strict mode (reject all basename collisions without --force)
    would break existing workflows. The issue's wording is ambiguous;
    sticking with opt-in behaviour and test abhigyanpatwari#6 locks that in. Flip to
    strict is one conditional change if reviewer prefers.
  - --as as a second spelling of --name (mentioned in the issue).
  - Rename path-hash suffix for list (full path is clearer for now).

Verification:
  npx vitest run test/unit/repo-manager.test.ts      -> 15 pass (9+6)
  npx vitest run test/unit/calltool-dispatch.test.ts -> 65 pass
  npm run test:unit                                   -> 3784 pass
    (2 pre-existing env failures in git-utils.test.ts unchanged)
  npx tsc --noEmit                                    -> clean

Closes abhigyanpatwari#829
@vercel

vercel Bot commented Apr 18, 2026

Copy link
Copy Markdown

@azizur100389 is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
6889 6792 0 97 235s

✅ All 6792 tests passed

97 test(s) skipped — expand for details
  • Swift MethodExtractor > isTypeDeclaration > recognizes class_declaration
  • Swift MethodExtractor > isTypeDeclaration > recognizes protocol_declaration
  • Swift MethodExtractor > isTypeDeclaration > rejects import_declaration
  • Swift MethodExtractor > visibility > extracts public method
  • Swift MethodExtractor > visibility > extracts private method
  • Swift MethodExtractor > visibility > defaults to internal when no modifier
  • Swift MethodExtractor > protocol methods > marks protocol method as abstract
  • Swift MethodExtractor > static and class methods > detects static func as isStatic
  • Swift MethodExtractor > static and class methods > detects class func as isStatic
  • Swift MethodExtractor > parameters > extracts parameters with types and default values
  • Swift MethodExtractor > return type > extracts return type from -> annotation
  • Swift MethodExtractor > annotations > extracts @objc attribute
  • Swift MethodExtractor > isFinal > detects final func
  • Swift MethodExtractor > isFinal > is false when not final
  • Swift MethodExtractor > isAsync > detects async func
  • Swift MethodExtractor > isOverride > detects override method
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift cross-file constructor inference uses lookupClassByName
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift explicit init inference uses lookupClassByName
  • buildTypeEnv > constructor inference (Tier 1 fallback) > lookupClassByName regression coverage > Swift lookupClassByName regression coverage > Swift cross-file constructor inference does not bind plain functions
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature
  • Swift constructor-inferred type resolution > detects User and Repo classes, both with save methods
  • Swift constructor-inferred type resolution > resolves user.save() to Models/User.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > resolves repo.save() to Models/Repo.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > emits exactly 2 save() CALLS edges (one per receiver type)
  • Swift self resolution > detects User and Repo classes, each with a save function
  • Swift self resolution > resolves self.save() inside User.process to User.save, not Repo.save
  • Swift parent resolution > detects BaseModel and User classes plus Serializable protocol
  • Swift parent resolution > emits EXTENDS edge: User → BaseModel
  • Swift parent resolution > emits IMPLEMENTS edge: User → Serializable (protocol conformance)
  • Swift cross-file User.init() inference > resolves user.save() via User.init(name:) inference
  • Swift cross-file User.init() inference > resolves user.greet() via User.init(name:) inference
  • Swift return type inference > detects User class and getUser function
  • Swift return type inference > detects save function on User (Swift class methods are Function nodes)
  • Swift return type inference > resolves user.save() to User#save via return type of getUser() -> User
  • Swift return-type inference via function return type > resolves user.save() to User#save via return type of getUser()
  • Swift return-type inference via function return type > user.save() does NOT resolve to Repo#save
  • Swift return-type inference via function return type > resolves repo.save() to Repo#save via return type of getRepo()
  • Swift implicit imports (cross-file visibility) > detects UserService class in Models.swift
  • Swift implicit imports (cross-file visibility) > resolves UserService() constructor call across files (no explicit import)
  • Swift implicit imports (cross-file visibility) > resolves service.fetchUser() member call across files
  • Swift implicit imports (cross-file visibility) > creates IMPORTS edges between files in the same module
  • Swift extension deduplication > detects Product class
  • Swift extension deduplication > resolves Product() constructor despite extension creating duplicate class node
  • Swift extension deduplication > resolves product.save() to Product.swift (primary definition)
  • Swift constructor call fallback (no new keyword) > resolves OCRService() as constructor call across files
  • Swift constructor call fallback (no new keyword) > resolves ocr.recognize() member call via constructor-inferred type
  • Swift export visibility (internal vs private) > resolves PublicService() constructor across files
  • Swift export visibility (internal vs private) > resolves internalHelper() across files (internal = module-scoped)
  • Swift if let / guard let binding resolution > detects User and Repo classes
  • Swift if let / guard let binding resolution > resolves user.save() inside if-let to User#save
  • Swift if let / guard let binding resolution > resolves repo.save() inside guard-let to Repo#save
  • Swift if let / guard let binding resolution > user.save() in if-let does NOT resolve to Repo#save
  • Swift await / try expression unwrapping > resolves user.save() via await fetchUser() return type
  • Swift await / try expression unwrapping > resolves repo.save() via try parseRepo() return type
  • Swift await / try expression unwrapping > detects fetchUser and parseRepo as functions
  • Swift for-in loop element type inference > detects User and Repo classes
  • Swift for-in loop element type inference > creates implicit import edges between files
  • Swift field-type resolution > detects classes and their properties
  • Swift field-type resolution > emits HAS_PROPERTY edges from class to field
  • Swift field-type resolution > resolves field-chain call user.address.save() → Address#save
  • Swift field-type resolution > emits ACCESSES edges for field reads in chains
  • Swift field-type resolution > populates field metadata (visibility, declaredType) on Property nodes
  • Swift call-result binding > resolves call-result-bound method call user.save() → User#save
  • Swift call-result binding > getUser() is present as a defined function
  • Swift call-result binding > emits processUser -> getUser CALLS edge for let-assigned free function call
  • Swift method enrichment > detects Animal protocol and Dog class
  • Swift method enrichment > emits IMPLEMENTS edge Dog -> Animal
  • Swift method enrichment > emits HAS_METHOD edges for Dog methods
  • Swift method enrichment > marks protocol Animal.speak as isAbstract
  • Swift method enrichment > marks Dog.speak as NOT isAbstract
  • Swift method enrichment > marks breathe as isFinal
  • Swift method enrichment > marks classify as isStatic
  • Swift method enrichment > captures @objc annotation on breathe
  • Swift method enrichment > populates parameterTypes for classify(_ name: String)
  • Swift method enrichment > records parameterCount for classify
  • Swift method enrichment > records returnType for speak
  • Swift method enrichment > resolves dog.speak() CALLS edge
  • Swift method enrichment > resolves Dog.classify("dog") CALLS edge
  • Swift abstract dispatch > detects Repository protocol and SqlRepository class
  • Swift abstract dispatch > emits IMPLEMENTS edge SqlRepository -> Repository
  • Swift abstract dispatch > emits HAS_METHOD edges for Repository.find and Repository.save
  • Swift abstract dispatch > emits HAS_METHOD edges for SqlRepository.find and SqlRepository.save
  • Swift abstract dispatch > marks base Repository.find as isAbstract
  • Swift abstract dispatch > marks base Repository.save as isAbstract
  • Swift abstract dispatch > marks concrete SqlRepository.find as NOT isAbstract
  • Swift abstract dispatch > resolves repo.find(id: 42) CALLS edge
  • Swift abstract dispatch > resolves repo.save(entity: user) CALLS edge
  • Swift abstract dispatch > populates parameterTypes for Repository.find
  • Swift abstract dispatch > populates parameterTypes for Repository.save
  • Swift abstract dispatch > records returnType for SqlRepository.find
  • Swift abstract dispatch > emits METHOD_IMPLEMENTS edges from SqlRepository methods → Repository protocol methods
  • Swift overloaded method disambiguation > detects 2 distinct find Method nodes on SqlRepository
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edges for both find overloads
  • Swift overloaded method disambiguation > emits METHOD_IMPLEMENTS edge for save
  • Swift overloaded method disambiguation > emits exactly 3 METHOD_IMPLEMENTS edges total
  • Swift Child extends Parent — inherited method resolution (SM-9) > detects Parent and Child classes
  • Swift Child extends Parent — inherited method resolution (SM-9) > resolves c.parentMethod() to Parent.parentMethod via first-wins MRO walk

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 73.55% 18306/24889 73.46% 📈 +0.1 🟢 ██████████████░░░░░░
Branches 62.39% 11580/18559 62.35% 📈 +0.0 🟢 ████████████░░░░░░░░
Functions 78.4% 1754/2237 78.16% 📈 +0.2 🟢 ███████████████░░░░░
Lines 76.11% 16592/21798 76.03% 📈 +0.1 🟢 ███████████████░░░░░

📋 View full run · Generated by CI

@magyargergo

Copy link
Copy Markdown
Collaborator

@claude Act as a senior reviewer for GitNexus. Your job is to determine whether this PR is production-ready for this repo, not to give a generic code review.

You are reviewing a PR in the GitNexus monorepo:

  • gitnexus/ → CLI + MCP
  • gitnexus-web/
  • gitnexus-shared/

Your task has 2 phases, in this exact order:

PHASE 1 — DEFINE THE BAR
Before reviewing the diff, establish a concise repo-specific definition of “production-ready” for GitNexus, based only on the repo docs and the affected area.
Keep this definition practical and reviewable. Do not invent standards that are not grounded in the repo.

PHASE 2 — REVIEW THE PR AGAINST THAT BAR
Review the actual diff only after defining the bar.
Stay tightly scoped to the changed code and its direct consequences.


CONTEXT TO LOAD FIRST
Read these before reviewing:

  • AGENTS.md
  • GUARDRAILS.md
  • CONTRIBUTING.md
  • TESTING.md
  • ARCHITECTURE.md

Additional context:


PRIMARY OBJECTIVE
Decide whether this PR is safe, correct, maintainable, and operationally acceptable to merge into production for GitNexus.

Do not optimize for completeness at the expense of signal.
Do not pad the review.
Do not propose unrelated refactors.
Do not restate the PR description unless needed for verification.


REVIEW RULES

  • Every finding must be grounded in specific evidence from the diff or directly relevant surrounding code.
  • Every finding must include path:line.
  • If you make a behavioral claim, cite the code that proves it.
  • If you make a performance claim, explain the mechanism.
  • If something cannot be verified from the diff alone, explicitly say so.
  • Distinguish clearly between:
    • verified issue
    • plausible risk
    • unverified concern
  • Avoid vague wording like “might be better” or “could be improved” unless you explain exactly why.
  • Keep the review focused on this PR’s scope only.

For each finding, assign one severity:

  • BLOCKING → must be fixed before merge
  • NON-BLOCKING → valid issue, but merge may still be acceptable
  • NIT → stylistic/minor, not merge-relevant

REPO-SPECIFIC REVIEW CHECKLIST
Use these exact headings.

1. Correctness & functional completeness

Check:

  • Does the implementation actually satisfy the PR claim?
    • ManifestExtractor is truly invoked
    • config.links produces non-zero cross-links where expected
  • Resolver contracts are preserved:
    • resolveSymbol remains exact-match
    • label-scoped Cypher remains correct per contract type
    • flag any regression toward fuzzy or unscoped matching
  • Graph schema integrity is preserved:
    • no silent changes to node labels
    • no silent changes to edge types
    • no silent changes to ID generation (generateId)
  • Call out any missing wiring, partial integration, dead branch, or mismatch between tests and runtime behavior

2. Code clarity & clean code

Check:

  • naming quality
  • local cohesion
  • dead code
  • unnecessary abstraction
  • hidden control flow
  • confusing indirection
  • adherence to repo conventions:
    • direct imports from gitnexus-shared
    • no barrel re-export regressions
    • no // removed comments
    • no unused re-exports
  • no drive-by refactors outside stated scope per CONTRIBUTING.md and GUARDRAILS.md § Scope

3. Test coverage & change safety

Evaluate against TESTING.md:

  • Are there unit tests under gitnexus/test/unit/ covering the newly wired path?
  • Is there a regression guard for 0-link → N-link behavior?
  • Are assertions meaningful rather than tautological?
  • Are fixtures realistic for manifest inputs?
  • If memoization/cache was introduced, is there a test proving hit/miss behavior and correctness?
  • Is there evidence the expected validation path would pass for staged gitnexus/ files?
    • tsc --noEmit
    • vitest run --project default
      If not verifiable, say exactly what is missing.

4. Performance

Inspect for:

  • hot-path overhead in ingestion/group sync
  • excess allocations per manifest entry
  • redundant Cypher round-trips
  • missed batching or missed parallelism (Promise.all) where it materially matters
  • O(n²) or repeated lookup patterns on large repos
  • memoization tradeoffs:
    • correctness
    • invalidation
    • bounded vs unbounded memory growth
      Do not speculate casually; explain the mechanism and likely impact.

5. Operational risk

Check:

  • Windows/cross-platform safety:
    • stream lifecycle
    • FD/file handle lifecycle
    • path separator assumptions
    • anything resembling prior ENOTEMPTY-style lifecycle regressions
  • LadybugDB single-writer invariant is preserved
  • Embeddings preservation:
    • no silent breakage of --embeddings
    • .gitnexus/meta.json.stats.embeddings not silently zeroed by changed paths
  • MCP contracts remain compatible:
    • group_*
    • query
    • context
    • impact
    • detect_changes
    • rename
    • cypher
      Flag any schema or contract break without migration note
  • staleness behavior still triggers correctly (gitnexus/src/mcp/staleness.ts)
  • rollback safety:
    • can this PR be reverted safely without re-analyze?
    • if not, explain why

6. Maintainability

Check:

  • Does the change respect Pipeline Phase DAG boundaries?
  • Is cross-phase coupling being introduced?
  • Are manifest-extractor entry points discoverable from group/sync.ts?
  • Would the next contributor know where to extend this?
  • Is shared logic placed appropriately:
    • in gitnexus-shared/
    • or gitnexus/src/core/**/utils.ts
    • and not duplicated between CLI and web bridge (server/api.ts)?

NON-NEGOTIABLES TO VERIFY
From GUARDRAILS.md / CONTRIBUTING.md:

  • no secrets, tokens, or machine-specific paths committed
  • no blind find-and-replace renames
  • if a symbol rename occurred, rename MCP tool or equivalent careful review was used
  • upstream impact was considered for modified shared symbols
  • no HIGH/CRITICAL impact left unaddressed
  • PR title follows conventional commit style + [area] Short description
  • PolyForm Noncommercial license headers/metadata untouched

OUTPUT FORMAT

Production-ready bar for this PR

Give a concise repo-specific definition in 5–10 bullets.

Findings

For each finding, use this structure exactly:

[BLOCKING|NON-BLOCKING|NIT] Short title

  • Evidence: path:line, path:line
  • Why it matters: ...
  • Recommendation: ...
  • Confidence: High / Medium / Low

Only include findings that are real and supported.
If there are no findings in a section, say:

  • No issues found in this area based on the diff and reviewed context.

What I could not fully verify

List any behavior you could not prove from the diff, tests, or available context.

Final verdict

Choose exactly one:

  • ✅ Ready to merge — no blocking issues.
  • ⚠️ Merge after addressing blocking items.
  • ❌ Not ready — fundamental concerns.

If verdict is not ready, list the blocking items in priority order.


IMPORTANT REVIEW BEHAVIOR

  • Be strict on correctness and contracts.
  • Be conservative about operational risk.
  • Be explicit about uncertainty.
  • Do not suggest unrelated refactors.
  • Do not turn nits into blockers.
  • Do not reward intent; review actual implementation.

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 4m 39s —— View job


PR Review — feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry

Tasks:

  • Load context (AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, ARCHITECTURE.md)
  • Read changed files
  • Define production-ready bar
  • Review against bar
  • Post final verdict

Production-ready bar for this PR

Based exclusively on AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, and ARCHITECTURE.md, a PR touching repo-manager.ts / CLI / resolveRepo is production-ready when:

  1. Registry schema backward-compat preserved — existing registry.json entries load unchanged; no on-disk migration required on rollback.
  2. registerRepo signature remains additive — third parameter optional; all existing call sites compile and behave identically without modification.
  3. resolveRepo and MCP error contracts not regressedimpact, context, query, cypher, detect_changes tool error messages remain actionable; no fuzzy-match regression.
  4. CLI flags do what their help text and error messages claim — misleading error suggestions are a correctness failure for a user-facing feature.
  5. Unit tests in gitnexus/test/unit/ cover the new code paths, assertions are meaningful (not tautological), and the registry is properly isolated per test.
  6. All CI checks passtsc --noEmit, vitest unit + integration on 3 platforms.
  7. No secrets, machine-specific paths, or PolyForm license metadata changes committed.
  8. PR title follows conventional-commit format (pr-labeler.yml enforces).
  9. No drive-by refactors outside the stated scope (CONTRIBUTING.md, GUARDRAILS.md § Scope).

Findings

1. Correctness & functional completeness

[BLOCKING] --force collision bypass is wired in the error message but not in the code

  • Evidence: run-analyze.ts:322, analyze.ts:321
  • Claim vs. implementation:
    • The CLI error handler at analyze.ts:321 explicitly tells the user:
      • Force the duplicate:     gitnexus analyze --force  (leaves "-r ${err.name}" ambiguous)
      
    • runFullAnalysis receives options.force (the re-index flag) and forwards it to the pipeline, but the call to registerRepo at run-analyze.ts:322 is:
      await registerRepo(repoPath, meta, { name: options.registryName });
      There is no force: options.force here. RegisterRepoOptions.force is never set from the CLI.
    • registerRepo checks opts?.force to decide whether to skip the collision guard (repo-manager.ts:339). Since it is never true from the CLI path, the guard fires regardless of --force.
  • Why it matters: A user who encounters the collision error and follows the documented suggestion (gitnexus analyze --force) will hit the same error again — because --force only controls pipeline re-indexing, not the name-collision bypass. The feature as documented does not work end-to-end.
  • Recommendation: One-line fix in run-analyze.ts:322:
    await registerRepo(repoPath, meta, { name: options.registryName, force: options.force });
    Note that AnalyzeOptions.force and RegisterRepoOptions.force are semantically different (re-index vs. allow-duplicate). Conflating them via the same --force flag is a design decision the PR author has already made; the fix is just the missing passthrough. Fix this →
  • Confidence: High

2. Code clarity & clean code

[NIT] RegistryNameCollisionError.name shadows Error.prototype.name

  • Evidence: repo-manager.ts:274,285, repo-manager.test.ts:219
  • Mechanism: public readonly name: string as a constructor parameter creates an instance property that shadows Error.prototype.name. The explicit this.name = name at line 285 is then redundant (TypeScript already assigned the parameter property), but both assignments produce the same outcome: err.name returns the registry alias (e.g., "shared") rather than "RegistryNameCollisionError". The test at repo-manager.test.ts:219 locks in and validates this behavior (expect(err.name).toBe('shared')).
  • Why it matters: By convention, Error.name identifies the error type in stack traces and tooling. The kind discriminant (repo-manager.ts:273) provides a reliable structural type guard, making instanceof plus kind safer than err.name for type checking. The current code happens to work because analyze.ts:319 reads err.name to display the alias — which is intentional — but the dual use is a trap for future callers who might check err.name === 'RegistryNameCollisionError'.
  • Recommendation: Rename the constructor parameter to registryName (or alias) and expose it as err.registryName to avoid overloading the inherited Error.name field. Update the error message string and test assertion accordingly.
  • Confidence: High

3. Test coverage & change safety

  • 6 new tests in gitnexus/test/unit/repo-manager.test.ts cover: alias storage (✅), alias preservation (✅), alias override (✅), collision throws and doesn't mutate registry (✅), force: true bypass (✅), backward-compat basename collisions (✅). Assertions are meaningful and registry is properly isolated via GITNEXUS_HOME.
  • Missing coverage for the BLOCKING bug: There is no test that exercises the full CLI path gitnexus analyze --force with a colliding name via runFullAnalysisregisterRepo. Test Embeddings pipeline #5 only calls registerRepo({ name, force: true }) directly. Had a test called runFullAnalysis with { force: true, registryName: 'shared' } after a collision was registered, the missing passthrough would have been caught.
  • CI passed: 6521 tests pass, tsc --noEmit clean, 3 platforms. No regression.

4. Performance

  • No issues found in this area based on the diff and reviewed context. The two new O(n) scans in registerRepo (one findIndex, one find) operate over a list of registered repos that is bounded by human usage (tens of entries in practice). No hot-path changes.

5. Operational risk

  • Windows path comparison asymmetry (plausible risk): The existingIdx finder at repo-manager.ts:322-325 uses case-insensitive comparison on Windows. The collision guard at repo-manager.ts:344 uses path.resolve(e.path) !== resolved — plain case-sensitive string equality regardless of platform. On Windows, two entries whose paths differ only by case would be found as the same entry by existingIdx (correctly de-duped), but in the collision scan they would be counted as distinct paths. This asymmetry is unlikely to matter in practice (Windows paths are normalized by path.resolve) but is a latent inconsistency introduced by this PR.
  • MCP contracts: resolveRepo error messages improved with collision-aware Available: hints. No tool schema changes. ✅
  • Rollback safety: Pure code revert, no on-disk migration needed. ✅
  • Embeddings, LadybugDB single-writer, staleness: Not touched by this PR. ✅
  • No secrets or machine-specific paths.

6. Maintainability

  • No Pipeline Phase DAG changes. No cross-phase coupling introduced. ✅
  • RegistryNameCollisionError and RegisterRepoOptions are in repo-manager.ts — correct location per ARCHITECTURE.md.
  • No shared logic that belongs in gitnexus-shared/.
  • Next contributor wiring a new analyze-variant (e.g., server-side worker) would need to know to forward force to registerRepo. The missing passthrough in run-analyze.ts sets a bad precedent. The blocking fix above addresses this.

Non-negotiables

Check Status
No secrets / machine-specific paths
No blind find-and-replace renames
PR title follows conventional-commit feat(cli): ...
PolyForm license metadata untouched
HIGH/CRITICAL impact left unaddressed N/A — no graph/schema changes

What I could not fully verify

  • End-to-end CLI behavior with --force --name <collision>: the test suite doesn't cover the runFullAnalysisregisterRepo path with both options set; I'm relying on static code analysis to confirm the passthrough is absent.
  • Windows case-insensitive path normalization by path.resolve: whether Windows path.resolve normalizes case such that the asymmetry in the collision guard is harmless in practice.
  • Behavior when a preserved-alias re-registration collides mid-session (user A aliased a repo, user B registers with the same alias on the same machine): correct per spec but the error message could mention the preserved-alias context to avoid user confusion.

Final verdict

⚠️ Merge after addressing blocking items.

Blocking item (priority order):

  1. run-analyze.ts:322 — add force: options.force to the registerRepo call. The error message analyze.ts:321 tells users gitnexus analyze --force bypasses the collision guard, but the flag is never forwarded. Users following the documented workaround will hit the same error again. Fix is one line.

The alias storage logic, collision guard, MCP improvements, backward-compat behavior, and test coverage are all correct and well-designed. The NIT on Error.name shadowing is worth addressing in a follow-up but is not merge-blocking.

Addresses the two findings from the @claude senior review on PR abhigyanpatwari#955.

BLOCKING — forward --force to registerRepo:

  The CLI error handler told users to re-run with `gitnexus analyze
  --force` to bypass the name-collision guard, but the flag was never
  threaded through runFullAnalysis -> registerRepo. A user hitting
  the collision who followed the documented workaround would hit the
  same error again, because AnalyzeOptions.force only controlled
  pipeline re-indexing, not the registry guard.

  Fix: pass `force: options.force` alongside `name: options.registryName`
  in the registerRepo call at core/run-analyze.ts. The two flags are
  semantically different (re-index vs. allow-duplicate) but the CLI
  intentionally reuses one --force switch for both; the missing
  passthrough was the gap.

NIT — RegistryNameCollisionError.name shadowed Error.prototype.name:

  The constructor parameter `public readonly name: string` created an
  instance property that shadowed the inherited `Error.name` class-name
  identifier. Downstream code doing `err.name === 'RegistryNameCollisionError'`
  would fail; the test at repo-manager.test.ts asserted `err.name ===
  'shared'` which locked in the wrong behaviour.

  Fix: rename the constructor parameter to `registryName` and expose
  it as `err.registryName`. Explicitly set `this.name` to the class
  name so `Error.prototype.name` semantics are preserved for type
  checks. Update analyze.ts catch block and the test assertion. Added
  a companion assertion that locks in `err.name ===
  'RegistryNameCollisionError'` so future regressions are caught.

Verification:
  npx vitest run test/unit/repo-manager.test.ts -> 15 pass
  npx tsc --noEmit                                -> clean

Not in scope for this commit:
  The reviewer suggested an integration-style test that exercises
  runFullAnalysis -> registerRepo end-to-end via a mocked registerRepo
  to lock in the force passthrough. Adding that would require mocking
  the ingestion pipeline stubs (git, LadybugDB, KuzuDB migration, FTS)
  which is a larger scaffolding lift than the 1-line bug fix. Left as
  a follow-up if demand surfaces.
@azizur100389

Copy link
Copy Markdown
Contributor Author

Thanks for the review — both findings addressed in ded8b94.

BLOCKING — --force now reaches registerRepo

Fixed in gitnexus/src/core/run-analyze.ts:

await registerRepo(repoPath, meta, {
  name: options.registryName,
  force: options.force,
});

The reviewer was right: the error-message workaround (gitnexus analyze --force) was dead-end because the flag stopped at runFullAnalysis and never reached the registry guard. One-line fix, now the documented escape hatch actually works.

NIT — RegistryNameCollisionError.name no longer shadows Error.prototype.name

  • Constructor param renamed nameregistryName
  • err.registryName carries the colliding alias
  • err.name restored to the inherited class-name semantics ('RegistryNameCollisionError')
  • analyze.ts catch block updated to read err.registryName
  • Test assertion updated, plus an added companion assertion expect(err.name).toBe('RegistryNameCollisionError') so a future regression that re-shadows would be caught.

Not in scope for this commit

Your suggestion for an integration-style test that calls runFullAnalysis with { force: true, registryName: 'shared' } and verifies the passthrough would have been the ideal regression guard — but adding it requires mocking the full ingestion pipeline (git, LadybugDB, KuzuDB migration, FTS). Happy to follow up with that scaffolding in a separate PR if you'd like, but left it out of this one to keep the review cycle tight.

Verification

  • npx vitest run test/unit/repo-manager.test.ts → 15 pass
  • npx tsc --noEmit → clean
  • Pre-commit hook passed

Also noted your "Windows path-comparison asymmetry" observation under Operational Risk — path.resolve normalizes case on Windows per the Node docs, so in practice the existing path.resolve equality works, but I agree it's a latent inconsistency. Happy to tighten that in a follow-up if you want — again, left out to keep this review cycle focused.

@magyargergo

Copy link
Copy Markdown
Collaborator

@claude Act as a senior reviewer for GitNexus. Your job is to determine whether this PR is production-ready for this repo, not to give a generic code review.

You are reviewing a PR in the GitNexus monorepo:

  • gitnexus/ → CLI + MCP
  • gitnexus-web/
  • gitnexus-shared/

Your task has 2 phases, in this exact order:

PHASE 1 — DEFINE THE BAR
Before reviewing the diff, establish a concise repo-specific definition of “production-ready” for GitNexus, based only on the repo docs and the affected area.
Keep this definition practical and reviewable. Do not invent standards that are not grounded in the repo.

PHASE 2 — REVIEW THE PR AGAINST THAT BAR
Review the actual diff only after defining the bar.
Stay tightly scoped to the changed code and its direct consequences.


CONTEXT TO LOAD FIRST
Read these before reviewing:

  • AGENTS.md
  • GUARDRAILS.md
  • CONTRIBUTING.md
  • TESTING.md
  • ARCHITECTURE.md

Additional context:


PRIMARY OBJECTIVE
Decide whether this PR is safe, correct, maintainable, and operationally acceptable to merge into production for GitNexus.

Do not optimize for completeness at the expense of signal.
Do not pad the review.
Do not propose unrelated refactors.
Do not restate the PR description unless needed for verification.


REVIEW RULES

  • Every finding must be grounded in specific evidence from the diff or directly relevant surrounding code.
  • Every finding must include path:line.
  • If you make a behavioral claim, cite the code that proves it.
  • If you make a performance claim, explain the mechanism.
  • If something cannot be verified from the diff alone, explicitly say so.
  • Distinguish clearly between:
    • verified issue
    • plausible risk
    • unverified concern
  • Avoid vague wording like “might be better” or “could be improved” unless you explain exactly why.
  • Keep the review focused on this PR’s scope only.

For each finding, assign one severity:

  • BLOCKING → must be fixed before merge
  • NON-BLOCKING → valid issue, but merge may still be acceptable
  • NIT → stylistic/minor, not merge-relevant

REPO-SPECIFIC REVIEW CHECKLIST
Use these exact headings.

1. Correctness & functional completeness

Check:

  • Does the implementation actually satisfy the PR claim?
    • ManifestExtractor is truly invoked
    • config.links produces non-zero cross-links where expected
  • Resolver contracts are preserved:
    • resolveSymbol remains exact-match
    • label-scoped Cypher remains correct per contract type
    • flag any regression toward fuzzy or unscoped matching
  • Graph schema integrity is preserved:
    • no silent changes to node labels
    • no silent changes to edge types
    • no silent changes to ID generation (generateId)
  • Call out any missing wiring, partial integration, dead branch, or mismatch between tests and runtime behavior

2. Code clarity & clean code

Check:

  • naming quality
  • local cohesion
  • dead code
  • unnecessary abstraction
  • hidden control flow
  • confusing indirection
  • adherence to repo conventions:
    • direct imports from gitnexus-shared
    • no barrel re-export regressions
    • no // removed comments
    • no unused re-exports
  • no drive-by refactors outside stated scope per CONTRIBUTING.md and GUARDRAILS.md § Scope

3. Test coverage & change safety

Evaluate against TESTING.md:

  • Are there unit tests under gitnexus/test/unit/ covering the newly wired path?
  • Is there a regression guard for 0-link → N-link behavior?
  • Are assertions meaningful rather than tautological?
  • Are fixtures realistic for manifest inputs?
  • If memoization/cache was introduced, is there a test proving hit/miss behavior and correctness?
  • Is there evidence the expected validation path would pass for staged gitnexus/ files?
    • tsc --noEmit
    • vitest run --project default
      If not verifiable, say exactly what is missing.

4. Performance

Inspect for:

  • hot-path overhead in ingestion/group sync
  • excess allocations per manifest entry
  • redundant Cypher round-trips
  • missed batching or missed parallelism (Promise.all) where it materially matters
  • O(n²) or repeated lookup patterns on large repos
  • memoization tradeoffs:
    • correctness
    • invalidation
    • bounded vs unbounded memory growth
      Do not speculate casually; explain the mechanism and likely impact.

5. Operational risk

Check:

  • Windows/cross-platform safety:
    • stream lifecycle
    • FD/file handle lifecycle
    • path separator assumptions
    • anything resembling prior ENOTEMPTY-style lifecycle regressions
  • LadybugDB single-writer invariant is preserved
  • Embeddings preservation:
    • no silent breakage of --embeddings
    • .gitnexus/meta.json.stats.embeddings not silently zeroed by changed paths
  • MCP contracts remain compatible:
    • group_*
    • query
    • context
    • impact
    • detect_changes
    • rename
    • cypher
      Flag any schema or contract break without migration note
  • staleness behavior still triggers correctly (gitnexus/src/mcp/staleness.ts)
  • rollback safety:
    • can this PR be reverted safely without re-analyze?
    • if not, explain why

6. Maintainability

Check:

  • Does the change respect Pipeline Phase DAG boundaries?
  • Is cross-phase coupling being introduced?
  • Are manifest-extractor entry points discoverable from group/sync.ts?
  • Would the next contributor know where to extend this?
  • Is shared logic placed appropriately:
    • in gitnexus-shared/
    • or gitnexus/src/core/**/utils.ts
    • and not duplicated between CLI and web bridge (server/api.ts)?

NON-NEGOTIABLES TO VERIFY
From GUARDRAILS.md / CONTRIBUTING.md:

  • no secrets, tokens, or machine-specific paths committed
  • no blind find-and-replace renames
  • if a symbol rename occurred, rename MCP tool or equivalent careful review was used
  • upstream impact was considered for modified shared symbols
  • no HIGH/CRITICAL impact left unaddressed
  • PR title follows conventional commit style + [area] Short description
  • PolyForm Noncommercial license headers/metadata untouched

OUTPUT FORMAT

Production-ready bar for this PR

Give a concise repo-specific definition in 5–10 bullets.

Findings

For each finding, use this structure exactly:

[BLOCKING|NON-BLOCKING|NIT] Short title

  • Evidence: path:line, path:line
  • Why it matters: ...
  • Recommendation: ...
  • Confidence: High / Medium / Low

Only include findings that are real and supported.
If there are no findings in a section, say:

  • No issues found in this area based on the diff and reviewed context.

What I could not fully verify

List any behavior you could not prove from the diff, tests, or available context.

Final verdict

Choose exactly one:

  • ✅ Ready to merge — no blocking issues.
  • ⚠️ Merge after addressing blocking items.
  • ❌ Not ready — fundamental concerns.

If verdict is not ready, list the blocking items in priority order.


IMPORTANT REVIEW BEHAVIOR

  • Be strict on correctness and contracts.
  • Be conservative about operational risk.
  • Be explicit about uncertainty.
  • Do not suggest unrelated refactors.
  • Do not turn nits into blockers.
  • Do not reward intent; review actual implementation.

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 2m 31s —— View job


PR Review — feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry

Tasks:

  • Load context (AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, ARCHITECTURE.md)
  • Read changed files (post-fix state: ded8b94)
  • Define production-ready bar
  • Review against bar
  • Post final verdict

Production-ready bar for this PR

Based on AGENTS.md, GUARDRAILS.md, CONTRIBUTING.md, TESTING.md, and the affected area (repo-manager.ts, CLI, resolveRepo):

  1. Registry schema backward-compat preservedregistry.json written by older versions loads unchanged; no migration required.
  2. registerRepo signature remains additive — third parameter optional; zero existing call sites require modification.
  3. resolveRepo and MCP error contracts not regressedimpact, context, query, cypher error messages remain actionable; no fuzzy-match regression.
  4. CLI flags do what their help text and error messages claim — especially the documented --force escape hatch.
  5. Unit tests in gitnexus/test/unit/ cover new code paths with meaningful, registry-isolated assertions.
  6. All CI checks passtsc --noEmit, vitest unit + integration on 3 platforms.
  7. No secrets, machine-specific paths, or PolyForm license metadata changes.
  8. PR title follows conventional-commit format (pr-labeler.yml enforces).
  9. No drive-by refactors outside stated scope (CONTRIBUTING.md, GUARDRAILS.md § Scope).

Findings

1. Correctness & functional completeness

Previous BLOCKING resolvedrun-analyze.ts:327-330 now correctly passes both fields:

await registerRepo(repoPath, meta, {
  name: options.registryName,
  force: options.force,
});

The commented block above this call (// Forward both name … and force … #829 review feedback) makes the intent explicit and durable. ✅

Previous NIT resolvedRegistryNameCollisionError (repo-manager.ts:278-291) no longer shadows Error.prototype.name. registryName is now a distinct field; this.name = 'RegistryNameCollisionError' is set explicitly post-super(). The test at repo-manager.test.ts:222-223 asserts both err.registryName === 'shared' and err.name === 'RegistryNameCollisionError'. ✅

[NON-BLOCKING] --skills flag inadvertently bypasses the collision guard

  • Evidence: analyze.ts:200, run-analyze.ts:327-330

  • Mechanism: analyze.ts:200 passes force: options?.force || options?.skills to runFullAnalysis. That combined value is forwarded as-is to registerRepo. Consequently, gitnexus analyze --name taken-alias --skills silently registers the duplicate without throwing RegistryNameCollisionError — even though the user did not pass --force. The collision guard checks opts?.force at repo-manager.ts:345, which is true whenever --skills is set.

  • Why it matters: The documented escape hatch for name collision is --force. Using --skills is not documented as having any bearing on the collision guard. A user who runs gitnexus analyze --name taken-alias --skills (e.g. in a CI script) would silently produce registry ambiguity that they did not consent to. The consequence is the same as --force — two entries with the same name — so operational damage is bounded. But the principle of least surprise is violated.

  • Recommendation: In run-analyze.ts, split the force semantics before forwarding to registerRepo:

    await registerRepo(repoPath, meta, {
      name: options.registryName,
      force: options.force,          // registry collision bypass: only the explicit --force flag
      // options.registryName already handles name precedence; skills re-index is already done
    });

    And in analyze.ts:200:

    force: options?.force || options?.skills,   // re-index flag (pipeline behavior)

    then add a separate field forceRegistry?: boolean that maps only from options?.force (not --skills). Or, simpler: keep force as-is for the pipeline, but at the registerRepo call site, pass force: options.forceRegistry ?? options.force if a distinct bit is not desirable for this PR.

    The cleanest minimal fix is not a one-liner here, so this is worth addressing in a follow-up rather than blocking this PR — especially since the condition requires --name <collision> --skills in the same invocation.

  • Confidence: High


2. Code clarity & clean code

  • No issues found. Naming is clean (registryName, hasCustomAlias, explicitName, kind discriminant). No dead code, no barrel re-export regressions, no // removed comments. The comment block at run-analyze.ts:322-326 explaining why force must reach registerRepo is a good durability measure.

3. Test coverage & change safety

  • 6 new unit tests isolate the registry via GITNEXUS_HOME, assertions are meaningful. Test Embeddings pipeline #4 (registerRepo throws … and registry is not mutated) is particularly valuable — it verifies the guard doesn't corrupt state on failure.
  • Test Embeddings pipeline #5 (force: true allows duplicate) verifies registerRepo directly. As the author noted, end-to-end coverage through runFullAnalysisregisterRepo with --force is missing, but adding it would require mocking the full pipeline. This gap is acknowledged and acceptable for this PR.
  • The assertion expect(err.name).toBe('RegistryNameCollisionError') at repo-manager.test.ts:223 now guards the NIT fix from regressing. ✅
  • CI: 6521 tests pass, tsc --noEmit clean, 3 platforms. ✅

4. Performance

  • No issues found. The two O(n) scans in registerRepo (one findIndex, one find) operate over a registry bounded by the number of manually registered repos — tens at most in practice. No hot-path changes.

5. Operational risk

  • --force passthrough confirmed — the documented workaround (gitnexus analyze --force) now correctly reaches the collision guard. ✅
  • MCP contracts unchangedresolveRepo error messages are additive only (disambiguated Available: hint). No tool schema changes. ✅
  • Rollback safety — pure code revert, no on-disk migration. registry.json written by this PR is byte-identical to older versions. ✅
  • Embeddings, LadybugDB single-writer, staleness — not touched. ✅
  • No secrets or machine-specific paths.
  • Windows path-comparison asymmetry (noted in the previous review) — path.resolve normalizes case on Windows per Node docs, so the path.resolve(e.path) !== resolved equality in the collision guard is safe in practice. The existing entry finder uses explicit case-insensitive compare; the collision guard does not — the asymmetry is latent but harmless.

6. Maintainability

  • No Pipeline Phase DAG changes; no cross-phase coupling introduced. ✅
  • RegistryNameCollisionError and RegisterRepoOptions in repo-manager.ts are the correct location per ARCHITECTURE.md.
  • The comment at run-analyze.ts:322-326 leaves a clear breadcrumb for any future contributor who wires another analyze variant. ✅
  • The NON-BLOCKING --skills/--force conflation is a latent trap for whoever next adds a flag that also forces a pipeline re-run — they would inadvertently inherit registry-guard bypass. A RunAnalysisOptions.forceRegistry field would cleanly separate the two intents.

Non-negotiables

Check Status
No secrets / machine-specific paths
No blind find-and-replace renames
PR title follows conventional-commit feat(cli): ...
PolyForm license metadata untouched
HIGH/CRITICAL impact unaddressed N/A — no graph/schema changes

What I could not fully verify

  • End-to-end --force behavior from CLI through runFullAnalysisregisterRepo: static analysis confirms the passthrough is wired; no integration test exercises the full CLI path.
  • --name <same-as-basename> aliasing behavior: hasCustomAlias returns false when the supplied --name equals the directory basename, so the alias is not persisted across re-analyses. This may be intentional (Option B) but is an undocumented edge case.

Final verdict

Ready to merge — no blocking issues.

The two previously flagged issues (BLOCKING: force not forwarded; NIT: Error.name shadowing) are correctly addressed in ded8b94. The NON-BLOCKING finding (--skills inadvertently bypasses the collision guard) is real but limited in impact and requires an unusual flag combination to trigger — acceptable as a follow-up.

@magyargergo

Copy link
Copy Markdown
Collaborator

@azizur100389 it's required to provide integration tests PRs like yours! Please add proper integration test

…ce + fix --skills bypass

Addresses two open items on PR abhigyanpatwari#955 raised by the senior review round 2
and by @magyargergo's follow-up comment:

1. magyargergo: "it's required to provide integration tests PRs like
   yours! Please add proper integration test." (taken on board — going
   forward every PR on this class of surface will ship with an
   integration test, not just unit tests.)

2. @claude review round 2 NON-BLOCKING: `--skills` inadvertently
   bypassed the collision guard because pipeline-force and
   registry-force were conflated.

Integration test (test/integration/cli-e2e.test.ts):

  End-to-end spawn-based test exercises the full CLI -> runFullAnalysis
  -> registerRepo chain across four steps:

  1. `gitnexus analyze --name shared` on repo A -> succeeds; registry
     entry created with name "shared"
  2. `gitnexus analyze --name shared` on repo B -> exits 1 with
     "Registry name collision" / "already used" on stderr
  3. `gitnexus analyze --name shared --force` on repo B -> succeeds;
     registry now has both entries under name "shared"
     (regression guard for the round-1 BLOCKING --force passthrough
      bug)
  4. `gitnexus analyze --name shared --skills` (no --force) on repo C
     -> exits 1 with collision error; registry still has only A + B
     (regression guard for the round-2 --skills bypass)

  Each step inspects registry.json directly, so the test locks in
  both functional behavior AND the passthrough wiring. Isolated
  GITNEXUS_HOME per-run so the developer's real ~/.gitnexus is never
  touched. Two new helpers (runCliWithEnv, makeMiniRepoCopy) reuse
  existing CLI-spawn + mini-repo-fixture infrastructure.

  Outer budget: 6 minutes (4 x ~60s analyze + fixture setup). All
  steps honor the CI-timeout `status === null` tolerance pattern.

--skills bypass fix (analyze.ts + run-analyze.ts):

  Pre-fix: analyze.ts passed `force: options?.force || options?.skills`
  to runFullAnalysis, and run-analyze.ts forwarded that OR'd bit to
  registerRepo. Consequence: `analyze --name taken --skills` silently
  bypassed the collision guard without the user passing --force.

  Fix: split the two concerns. AnalyzeOptions.force keeps
  pipeline-re-index semantics (still OR'd with --skills; skills needs
  a fresh pipelineResult). New AnalyzeOptions.registryForce is
  populated ONLY from the real --force flag; run-analyze.ts passes
  registryForce to registerRepo. Inline comments at both call sites
  document the split so future flag additions don't re-conflate.

Verification:
  npx vitest run test/integration/cli-e2e.test.ts -t "analyze --name"
    -> 1 pass (70s, all 4 steps exercised against real CLI)
  npx vitest run test/unit/repo-manager.test.ts
    -> 15 pass
  npm run test:unit
    -> 3778 pass, 2 pre-existing git-utils env failures unchanged
  npx tsc --noEmit
    -> clean
  Pre-commit hook (lint-staged + prettier + tsc) -> clean
@azizur100389

Copy link
Copy Markdown
Contributor Author

Thanks @magyargergo — you were right to push on this. I should have shipped the integration test with the original PR rather than proposing it as a follow-up. Going forward I'll default to shipping an integration test alongside any PR that touches CLI / MCP / storage surfaces; unit tests alone don't catch wiring bugs like the --force passthrough (round 1) or the --skills bypass (round 2).

Commit 36d6625 adds both:

  • End-to-end CLI integration test — spawns real CLI across 4 steps against isolated GITNEXUS_HOME + colliding-basename mini-repo copies. Steps 3 and 4 are regression guards for the round-1 --force passthrough bug and the round-2 --skills bypass respectively.
  • --skills bypass fix — split AnalyzeOptions.force (pipeline re-index, still OR'd with --skills) from new AnalyzeOptions.registryForce (populated only from the real --force flag). Inline comments at both call sites so future flags don't re-conflate.

Verification: integration test passes (70s, 4 steps), 15 unit tests pass, 3778 full-suite pass, tsc --noEmit clean, pre-commit hook clean.

@magyargergo

Copy link
Copy Markdown
Collaborator

Please make sure the CI is healthy

…anpatwari#955 CI)

macOS and windows-latest CI failed on the new integration test because
of platform-specific path resolution asymmetries:

 * macOS: os.tmpdir() returns /var/folders/... but spawnSync child
   processes resolve the symlink to /private/var/folders/... when
   reading process.cwd() — so registerRepo stores the realpath while
   the test's path.resolve(repoA) kept the symlink form.
     Expected: /var/folders/tb/.../collide-app
     Actual:   /private/var/folders/tb/.../collide-app

 * Windows: os.tmpdir() can return the 8.3 short-name form
   (C:\Users\RUNNER~1\...) while the spawned CLI process sees the long
   form (C:\Users\runneradmin\...).
     Expected: C:\Users\RUNNER~1\...\collide-app
     Actual:   C:\Users\runneradmin\...\collide-app

 * Ubuntu passed because /tmp is not symlinked on GitHub runners.

Fix: introduce a local `canonical(p)` helper that calls
fs.realpathSync(), which normalizes both macOS symlinks AND Windows
8.3 short names to the canonical form. Applied to all three path
assertions in the integration test. No source changes — the fix is
in the test harness only.

Verification:
  npx vitest run test/integration/cli-e2e.test.ts -t "analyze --name"
    -> 1 pass locally (76s on Windows worktree, same platform that
       failed in CI before the fix)
  npx tsc --noEmit -> clean
  Pre-commit hook -> clean
@azizur100389

azizur100389 commented Apr 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @magyargergo — CI failure diagnosed and fix pushed in 76431bf.

Root cause: platform-specific tmpdir path asymmetry in the new integration test:

  • macOS: os.tmpdir() returns /var/folders/... but spawnSync child processes resolve the symlink to /private/var/folders/... when reading process.cwd() — so registerRepo stored the realpath while the test's path.resolve(repoA) kept the symlink form.
  • Windows: os.tmpdir() returns the 8.3 short form (C:\Users\RUNNER~1\...) on the CI runner but the spawned CLI sees the long form (C:\Users\runneradmin\...).
  • Ubuntu passed because /tmp isn't symlinked on GitHub runners.

Fix: canonicalise both sides of each path-equality assertion via fs.realpathSync() — the Node-standard cross-platform way to normalise both symlinks and 8.3 short names. Test-harness-only change; no source code touched.

Honest verification statement:

  • I only have a Windows worktree locally, and crucially my local Windows os.tmpdir() returns the long form, not the 8.3 short form the GitHub runner uses. So my local re-run (76s, pass) does not prove the fix works on the failing CI platforms — it only proves I haven't broken the Ubuntu-equivalent path.
  • I did not run the tests on macOS; I don't have access to one. The fix is reasoned from fs.realpathSync() documentation and the exact error strings in the CI log, not empirically verified on macOS.
  • CI is the empirical verification. I'll own any regression if this fails again on macOS or windows-latest and follow up immediately.

Comment thread gitnexus/src/cli/analyze.ts Outdated
Comment on lines +209 to +214
// Registry-collision bypass — only the explicit --force flag.
// Keeping this separate from `force` above means --skills (and any
// future flag that triggers pipeline re-run) does NOT accidentally
// bypass the RegistryNameCollisionError guard. See #829 review
// round 2.
registryForce: options?.force,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the right direction! Since, --force re-indexes the repo and users may not want to do that. I'd recommend using a different option from the --force.

…ate-name (abhigyanpatwari#955)

Per review feedback on abhigyanpatwari#955: --force re-indexes the repo, which users
may not want when they just need to register a duplicate alias.

- New CLI flag: --allow-duplicate-name (registry bypass only, no re-index)
- Renamed RegisterRepoOptions.force -> allowDuplicateName end-to-end
- run-analyze.ts keeps options.force for pipeline re-index decisions;
  registry path now reads options.allowDuplicateName independently
- Collision error hint updated to suggest --allow-duplicate-name
- Integration test step 4 now probes --skills (not --force) as the
  "pipeline-rerun must not silently bypass the guard" case
@azizur100389

azizur100389 commented Apr 18, 2026

Copy link
Copy Markdown
Contributor Author

@magyargergo — good catch, you're right. --force triggering a full pipeline re-index is a side effect users don't want when all they're doing is registering a duplicate alias. The two concerns are orthogonal and shouldn't share a flag.

Pushed 915271c which splits them:

  • --force — back to its original semantics (what it was on main before this PR): re-run the ingestion pipeline only. options.force in run-analyze.ts again gates only the pipeline-re-index branches; it no longer reaches registerRepo.
  • --allow-duplicate-name — new, registry-only: permits registerRepo to write an entry whose alias is already taken by another path. Does not touch the pipeline.

End-to-end rename across the PR: RegisterRepoOptions.forceallowDuplicateName, and runAnalyze's registryForce (introduced in a prior round of this PR to prove the pipeline-vs-registry split was needed internally) → allowDuplicateName. The collision error hint now suggests --allow-duplicate-name (not --force).

Integration test update: step 3 previously called analyze --name shared --force to prove the registry bypass worked; it now calls analyze --name shared --allow-duplicate-name. Step 4 — which was already using --skills to prove that a pipeline-re-index flag does not silently bypass the guard — is unchanged, and now asserts the error message nudges the user toward --allow-duplicate-name.

Local verification: tsc --noEmit clean · full unit suite 3785 pass (2 pre-existing git-utils env failures unchanged, not touched here) · repo-manager.test.ts 15/15 · cli-e2e.test.ts --name integration it() block green (4 steps).

Thanks for the review — the split reads much cleaner.

Comment thread gitnexus/src/storage/repo-manager.ts Outdated
// list output this PR also ships).
const explicitName = opts?.name !== undefined || (existing && hasCustomAlias(existing));
if (explicitName && !opts?.allowDuplicateName) {
const collision = entries.find(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const collision = entries.find(
const collision = entries.some(

I think, it's more appropriate.

@azizur100389 azizur100389 Apr 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — fair signal on the noise. I kept find rather than some on purpose: line 355 passes the matched entry's path into the RegistryNameCollisionError constructor so the user sees exactly which existing path already owns the alias (and can disambiguate with -r <path>). With some, we'd only get a boolean and would need a second pass over entries to build that error — two scans for one check.

Worth flagging that the informative "already used by /path/to/X" message is a deliberate feature of this PR, not incidental: issue #829 explicitly called out the misleading "Available: app, backend, website" hint as one of the bugs to fix, so losing "by which path" in the error would undermine the fix.

To address what I think was the real reviewer-signal (the variable name reading boolean-ish): in 553ee0e I renamed collisioncollidingEntry. Same semantics, but the name now makes it obvious the value is the matched entry, not a yes/no, which is why .find() is the operator we want here.

* the default basename-derived registry `name`. Persisted — subsequent
* re-analyses of the same path without `--name` preserve the alias.
*/
name?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly I can make multiple analysis on one repo and has multiple aliases with this option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — no. One repo path always maps to exactly one registry entry, and therefore exactly one alias.

Concretely, registerRepo upserts by resolved path: entries.findIndex(...) on gitnexus/src/storage/repo-manager.ts:329 locates the existing entry for this path, and the write branch at gitnexus/src/storage/repo-manager.ts:368 either updates that slot in place or pushes a new one — never both. So running analyze --name Y on a repo that was previously --name X replaces X with Y rather than adding a second entry. That invariant is locked in by the re-registerRepo with a different name overrides the previous alias test at repo-manager.test.ts:196.

--allow-duplicate-name operates in the orthogonal direction: it permits two different paths to share one alias (rendering -r <alias> ambiguous between those two paths, at which point the user falls back to -r <path>, which already works).

In 553ee0e I expanded the JSDoc on this field to spell that scope out explicitly — "cross-path alias sharing only · one path → one entry always · --name Y on the same path overwrites --name X" — so the next reader doesn't have to reconstruct it from the code.

abhigyanpatwari#955 review)

Address inline review comments from @magyargergo:

1. Rename `collision` -> `collidingEntry` in registerRepo's duplicate
   guard. Zero behaviour change; signals the value is the registry
   entry object (consumed as `collidingEntry.path` when constructing
   RegistryNameCollisionError), which is precisely why `.find()` is
   the right operator rather than `.some()`.

2. Expand the JSDoc on RegisterRepoOptions.allowDuplicateName to
   explicitly state the flag's scope: it permits TWO DIFFERENT repo
   paths to share one alias; one repo path always has exactly one
   registry entry / one alias (registerRepo upserts by resolved path).
   Re-analyzing the same path with `--name Y` overwrites a previous
   `--name X` — already locked in by the
   "re-registerRepo with a different name overrides the previous
   alias" unit test.
@magyargergo

Copy link
Copy Markdown
Collaborator

@claude does this PR resolves the issue ticket as it promises?

@github-actions

github-actions Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 1m 24s —— View job


Does this PR resolve issue #829?

Tasks:


Summary

Yes — the PR fully resolves the three problems described in issue #829. Here's a point-by-point verification against each bug and the stated fixes:


Issue #829 — Three root problems

Problem 1: -r app is ambiguous when two repos share a basename

Fixed. The PR adds analyze --name <alias> (cli/index.ts:31-35) so users can register each repo under a distinct name. The alias is persisted via hasCustomAlias precedence logic in registerRepo (repo-manager.ts:347-348), so re-running analyze without --name preserves a previously set alias.


Problem 2: Available: app, app, backend error hint is misleading

Fixed. resolveRepo in local-backend.ts:323-334 now builds a disambiguated label list. When two handles share a name, each colliding entry gets name (path) appended, producing:

Available: app (/tmp/gnx-a/app), app (/tmp/gnx-b/app), backend

Unique-name entries render byte-identically to before this PR.


Problem 3: gitnexus list output identical for two repos with same name

Fixed. list.ts:22-34 counts name occurrences and adds a name (path) header for any entry involved in a collision. Non-colliding entries are unchanged.


Bonus item (already worked, regression-tested): -r <absolute-path>

Confirmed. resolveRepoFromCache at local-backend.ts:358-362 already handled absolute paths via path.resolve. The integration test exercises this path for regression coverage.


Key design decisions — verified correct

Claim Status
--allow-duplicate-name is a separate flag from --force ✅ Confirmed — run-analyze.ts:337-345 passes allowDuplicateName: options.allowDuplicateName independently; --force/--skills don't reach the registry guard
--skills does NOT bypass the collision guard ✅ Confirmed — analyze.ts:211 OR's force||skills for pipeline only; the registry call at run-analyze.ts:342-345 passes allowDuplicateName independently. Integration test step 4 (cli-e2e.test.ts:321-332) explicitly guards this
Backward compat — basename collisions without --name still register silently ✅ Confirmed — repo-manager.ts:356: explicitName is false when neither opts.name nor a preserved alias exists, so the guard is skipped
Alias is preserved on re-analyze without --name ✅ Confirmed — repo-manager.ts:347-348 prefers preserved alias over basename when hasCustomAlias is true
RegistryNameCollisionError.name doesn't shadow Error.prototype.name ✅ Fixed in ded8b94registryName field carries the alias; this.name = 'RegistryNameCollisionError' restored; test at repo-manager.test.ts:222-223 guards it
registry.json schema unchanged — rollback safe ✅ No new fields; old registries load unchanged

Test coverage

  • 15 unit tests in repo-manager.test.ts (9 existing + 6 new) — cover alias storage, preservation, override, collision throw + no-mutation, allowDuplicateName bypass, and backward-compat basename collisions.
  • 4-step integration test in cli-e2e.test.ts:226-345 — spawns the real CLI against isolated GITNEXUS_HOME, exercises the full CLI → runFullAnalysis → registerRepo chain for all four cases.
  • CI: 6792 tests pass on 3 platforms, tsc --noEmit clean.

Verdict

The PR delivers exactly what #829 asked for: a --name alias, a collision guard with a clear escape hatch (--allow-duplicate-name), and disambiguated output in both list and resolveRepo error messages. All three root problems are addressed, the design is backward-compatible, and the implementation is well-tested end-to-end.

@magyargergo magyargergo merged commit dae7bd3 into abhigyanpatwari:main Apr 19, 2026
16 of 17 checks passed
@magyargergo

Copy link
Copy Markdown
Collaborator

Thank you for this quality work! Please keep up your great contribution coming. :)

@azizur100389

Copy link
Copy Markdown
Contributor Author

Thanks, Gergő — really appreciated. Will send more!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

analyze has no way to disambiguate repos with the same basename — -r <name> becomes ambiguous

2 participants