Merge branch 'main' into codex/prune-local-const-symbols

This commit is contained in:
Gergő Magyar 2026-06-07 10:57:34 +01:00 • committed by GitHub
commit 03e2a15fba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 912 additions and 455 deletions

View file

@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@ -40,7 +40,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@ -48,24 +48,24 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
@ -75,11 +75,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow

View file

@ -18,8 +18,8 @@ description: "Use when the user asks how code works, wants to understand archite
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@ -47,18 +47,18 @@ description: "Use when the user asks how code works, wants to understand archite
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@ -68,10 +68,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details

View file

@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@ -55,10 +55,10 @@ description: "Use when the user wants to know what will break if they change som
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@ -73,10 +73,10 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@ -86,7 +86,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)

View file

@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:

View file

@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
@ -98,24 +98,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

View file

@ -80,18 +80,18 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER edit a function, class, or method without first running `impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
## Resources

View file

@ -62,18 +62,18 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER edit a function, class, or method without first running `impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
## Resources

View file

@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@ -40,7 +40,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@ -48,24 +48,24 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
@ -75,11 +75,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow

View file

@ -18,8 +18,8 @@ description: "Use when the user asks how code works, wants to understand archite
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@ -47,18 +47,18 @@ description: "Use when the user asks how code works, wants to understand archite
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@ -68,10 +68,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details

View file

@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@ -55,10 +55,10 @@ description: "Use when the user wants to know what will break if they change som
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@ -73,10 +73,10 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@ -86,7 +86,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)

View file

@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:

View file

@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
@ -98,24 +98,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

View file

@ -15,10 +15,10 @@ description: Trace bugs through call chains using knowledge graph
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
@ -27,11 +27,11 @@ description: Trace bugs through call chains using knowledge graph
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@ -39,7 +39,7 @@ description: Trace bugs through call chains using knowledge graph
| Symptom | GitNexus Approach |
|---------|-------------------|
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@ -47,22 +47,22 @@ description: Trace bugs through call chains using knowledge graph
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
@ -71,11 +71,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow

View file

@ -17,8 +17,8 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
@ -28,9 +28,9 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@ -46,16 +46,16 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@ -65,10 +65,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details

View file

@ -16,9 +16,9 @@ description: Analyze blast radius before making code changes
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
@ -27,11 +27,11 @@ description: Analyze blast radius before making code changes
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@ -54,9 +54,9 @@ description: Analyze blast radius before making code changes
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@ -71,9 +71,9 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@ -83,7 +83,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)

View file

@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:

View file

@ -15,9 +15,9 @@ description: Plan safe refactors using blast radius and dependency mapping
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
@ -27,60 +27,60 @@ description: Plan safe refactors using blast radius and dependency mapping
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
@ -90,24 +90,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
|-------------|------------|
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

View file

@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@ -40,7 +40,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@ -48,24 +48,24 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
@ -75,11 +75,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow

View file

@ -18,8 +18,8 @@ description: "Use when the user asks how code works, wants to understand archite
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@ -47,18 +47,18 @@ description: "Use when the user asks how code works, wants to understand archite
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@ -68,10 +68,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details

View file

@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@ -55,10 +55,10 @@ description: "Use when the user wants to know what will break if they change som
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@ -73,10 +73,10 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@ -86,7 +86,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)

View file

@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:

View file

@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
@ -98,24 +98,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```

View file

@ -174,18 +174,18 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run \`gitnexus_detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`gitnexus_detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run \`detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`gitnexus_context({name: "symbolName"})\`.
- When exploring unfamiliar code, use \`query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`context({name: "symbolName"})\`.
## Never Do
- NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
- NEVER edit a function, class, or method without first running \`impact\` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use \`gitnexus_rename\` which understands the call graph.
- NEVER commit changes without running \`gitnexus_detect_changes()\` to check affected scope.
- NEVER rename symbols with find-and-replace — use \`rename\` which understands the call graph.
- NEVER commit changes without running \`detect_changes()\` to check affected scope.
## Resources

View file

@ -649,9 +649,9 @@ const renderSkillMarkdown = (
: community.label;
lines.push('## How to Explore');
lines.push('');
lines.push(`1. \`gitnexus_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
lines.push(`1. \`context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
lines.push(
`2. \`gitnexus_query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
`2. \`query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
);
lines.push('3. Read key files listed above for implementation details');
lines.push('');

View file

@ -8,6 +8,7 @@
import fs from 'fs/promises';
import path from 'path';
import { createHash } from 'crypto';
import {
initLbug,
executeQuery,
@ -298,9 +299,16 @@ export function resolveWorktreeCwd(repoPath: string, launchCwd: string): string
}
/**
* Length of the base64url path hash appended to a colliding repo id.
* Length of the path-derived suffix appended to a colliding repo id.
* Exported so tests can pin the suffix shape without re-deriving the
* literal; see `repoId()` and the hashed-id resolution tier (#1658).
* literal; see `assignRepoId()` and the hashed-id resolution tier (#1658).
*
* Note: base64url is an *encoding*, not a hash — it preserves byte order, so
* two paths that share a long common prefix (sibling clones under one parent)
* collapse to the same sliced suffix. `assignRepoId()` keeps the legacy
* base64url suffix only for the first colliding duplicate (id compatibility)
* and falls back to a content hash of the resolved path on a real collision
* (#2054).
*/
export const REPO_ID_HASH_LENGTH = 6;
@ -382,11 +390,35 @@ export class LocalBackend {
*/
private async refreshRepos(): Promise<void> {
const entries = await listRegisteredRepos({ validate: true });
const freshIds = new Set<string>();
for (const entry of entries) {
const id = this.repoId(entry.name, entry.path);
freshIds.add(id);
// Build the next map from scratch and swap it in atomically. Mutating the
// live map in place let stale entries influence fresh id assignment: a
// bare-name id, once handed to the first registry entry, stuck to it across
// refreshes and reorders, and colliding path suffixes silently overwrote
// each other so sibling clones disappeared from `list_repos` (#2054).
const nextRepos = new Map<string, RepoHandle>();
const nextContext = new Map<string, CodebaseContext>();
const assigned = new Map<string, string>(); // id -> resolved repo path
// Assign ids over a path-sorted view so a registered clone always gets the
// same id regardless of the registry's on-disk order: the bare name and
// each path-derived suffix become a pure function of the resolved-path set,
// not of iteration order, so a memorized id can't drift to a different
// clone after a registry reorder (#2067 follow-up).
const ordered = [...entries].sort((a, b) => {
const ra = path.resolve(a.path);
const rb = path.resolve(b.path);
return ra < rb ? -1 : ra > rb ? 1 : 0;
});
for (const entry of ordered) {
// path.resolve (not canonicalizePath) matches the pre-#2054 collision
// check and keeps refreshRepos free of mockable deps on the hot init
// path. registerRepo writes path.resolve'd paths (not realpath), and
// resolveRepoFromCache canonicalizes both sides when matching by path, so
// keying id assignment on path.resolve here is consistent and correct.
const resolved = path.resolve(entry.path);
const id = this.assignRepoId(entry.name, entry.path, resolved, assigned);
const storagePath = entry.storagePath;
const lbugPath = path.join(storagePath, 'lbug');
@ -412,11 +444,11 @@ export class LocalBackend {
stats: entry.stats,
};
this.repos.set(id, handle);
nextRepos.set(id, handle);
// Build lightweight context (no LadybugDB needed)
const s = entry.stats || {};
this.contextCache.set(id, {
nextContext.set(id, {
projectName: entry.name,
stats: {
fileCount: s.files || 0,
@ -427,37 +459,87 @@ export class LocalBackend {
});
}
// Prune repos that no longer exist in the registry
for (const id of this.repos.keys()) {
if (!freshIds.has(id)) {
this.repos.delete(id);
this.contextCache.delete(id);
this.initializedRepos.delete(id);
}
// Prune per-clone pool state for databases that are no longer registered.
// The LadybugDB pool and the init/staleness/reinit maps are keyed by the
// immutable lbugPath (see ensureInitialized), so a repo id that merely
// moves to a different clone needs NO eviction — distinct clones have
// distinct lbugPaths and can never share a pool entry, which is what closes
// the resolve→query wrong-clone window for good (#2067). Only a path that
// dropped out of the registry must release its pooled connection + state.
const liveLbugPaths = new Set([...nextRepos.values()].map((h) => h.lbugPath));
for (const prev of this.repos.values()) {
if (liveLbugPaths.has(prev.lbugPath)) continue;
this.initializedRepos.delete(prev.lbugPath);
this.lastStalenessCheck.delete(prev.lbugPath);
this.reinitPromises.delete(prev.lbugPath);
closeLbug(prev.lbugPath).catch(() => {});
}
this.repos = nextRepos;
this.contextCache = nextContext;
}
/**
* Generate a stable repo ID from name + path.
* If names collide, append a hash of the path.
* Assign a collision-free in-memory id for a registered repo.
*
* - Unique name → the bare lowercased name.
* - Duplicate name → a path-derived suffix. The *first* colliding clone keeps
* the legacy `base64url(path)` suffix so ids generated before #2054 still
* resolve (the #1658 hashed-id tier). base64url is an encoding, not a hash:
* it preserves byte order, so sibling clones under one parent (e.g.
* `.../REPO_2` and `.../REPO_3`) yield identical leading characters and thus
* the same sliced suffix. Any further collision therefore falls back to a
* content hash of the *resolved* path (order-insensitive), extended
* deterministically until unique.
*
* `assigned` maps every id handed out in this refresh to its resolved path,
* so a candidate is "free" when it is unused or already owned by this exact
* path. This method records its own assignment into `assigned` before
* returning, so the map-update is the function's invariant, not a caller
* obligation. A returned id never overwrites a different path's handle (#2054).
*/
private repoId(name: string, repoPath: string): string {
private assignRepoId(
name: string,
repoPath: string,
resolved: string,
assigned: Map<string, string>,
): string {
const base = name.toLowerCase();
// Check for name collision with a different path
for (const [id, handle] of this.repos) {
if (id === base && handle.repoPath !== path.resolve(repoPath)) {
// Collision — use path hash
// Lowercase the hash so it survives the `paramLower` lookup in
// resolveRepoFromCache — base64url retains mixed case, but the id
// tier compares against `repoParam.toLowerCase()` (#1658 follow-up).
const hash = Buffer.from(repoPath)
.toString('base64url')
.slice(0, REPO_ID_HASH_LENGTH)
.toLowerCase();
return `${base}-${hash}`;
}
const free = (id: string): boolean => {
const owner = assigned.get(id);
return owner === undefined || owner === resolved;
};
// Record the assignment so subsequent entries in the same refresh see this
// id as taken (the function owns its own invariant).
const claim = (id: string): string => {
assigned.set(id, resolved);
return id;
};
if (free(base)) return claim(base);
// Legacy suffix from the *raw* path — kept byte-for-byte so the first
// colliding duplicate keeps the id it had before #2054 (#1658 tier).
const legacy = `${base}-${Buffer.from(repoPath)
.toString('base64url')
.slice(0, REPO_ID_HASH_LENGTH)
.toLowerCase()}`;
if (free(legacy)) return claim(legacy);
// Real collision — hash the resolved path. Lowercase hex survives the
// `paramLower` lookup in resolveRepoFromCache.
const digest = createHash('sha256').update(resolved).digest('hex');
for (let len = REPO_ID_HASH_LENGTH; len <= digest.length; len++) {
const candidate = `${base}-${digest.slice(0, len)}`;
if (free(candidate)) return claim(candidate);
}
return base;
// Two distinct resolved paths sharing a full SHA-256 digest is a hash
// break, not a runtime condition — fail loudly rather than silently
// overwrite a different repo's handle (#2054 invariant).
throw new Error(
`GitNexus internal: unable to assign a unique repo id for "${name}" at ${repoPath}`,
);
}
// ─── Repo Resolution ─────────────────────────────────────────────
@ -641,43 +723,51 @@ export class LocalBackend {
// ─── Lazy LadybugDB Init ────────────────────────────────────────────
private async ensureInitialized(repoId: string): Promise<void> {
/**
* Ensure the LadybugDB pool is open for the *resolved* repo.
*
* Takes the `RepoHandle` the caller resolved — NOT a bare id — and keys the
* pool (and the init/staleness/reinit maps) by the immutable `lbugPath`. Two
* things matter for multi-clone correctness: (1) the handle is the one the
* caller resolved, so a concurrent `refreshRepos` can't substitute a different
* clone; (2) the pool key is the database path, so distinct clones never share
* a pool entry even when their name-derived id transiently collides (#2067).
*/
private async ensureInitialized(repo: RepoHandle): Promise<void> {
const poolKey = repo.lbugPath;
// If a reinit is already in progress for this repo, wait for it
const pending = this.reinitPromises.get(repoId);
const pending = this.reinitPromises.get(poolKey);
if (pending) return pending;
const handle = this.repos.get(repoId);
if (!handle) throw new Error(`Unknown repo: ${repoId}`);
// Check if the index was rebuilt since we opened the connection (#297).
// Throttle staleness checks to at most once per 5 seconds per repo to
// avoid an fs.readFile round-trip on every tool invocation.
if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) {
if (this.initializedRepos.has(poolKey) && isLbugReady(poolKey)) {
const now = Date.now();
const lastCheck = this.lastStalenessCheck.get(repoId) ?? 0;
const lastCheck = this.lastStalenessCheck.get(poolKey) ?? 0;
if (now - lastCheck < 5000) return; // Checked recently — skip
this.lastStalenessCheck.set(repoId, now);
this.lastStalenessCheck.set(poolKey, now);
try {
const metaPath = path.join(handle.storagePath, 'meta.json');
const metaPath = path.join(repo.storagePath, 'meta.json');
const metaRaw = await fs.readFile(metaPath, 'utf-8');
const meta = JSON.parse(metaRaw);
if (meta.indexedAt && meta.indexedAt !== handle.indexedAt) {
if (meta.indexedAt && meta.indexedAt !== repo.indexedAt) {
// Index was rebuilt — close stale connection and re-init.
// Wrap in reinitPromises to prevent TOCTOU race where concurrent
// callers both detect staleness and double-close the pool.
const reinit = (async () => {
try {
await closeLbug(repoId);
this.initializedRepos.delete(repoId);
handle.indexedAt = meta.indexedAt;
await initLbug(repoId, handle.lbugPath);
this.initializedRepos.add(repoId);
await closeLbug(poolKey);
this.initializedRepos.delete(poolKey);
repo.indexedAt = meta.indexedAt;
await initLbug(poolKey, repo.lbugPath);
this.initializedRepos.add(poolKey);
} finally {
this.reinitPromises.delete(repoId);
this.reinitPromises.delete(poolKey);
}
})();
this.reinitPromises.set(repoId, reinit);
this.reinitPromises.set(poolKey, reinit);
return reinit;
} else {
return; // Pool is current
@ -688,11 +778,11 @@ export class LocalBackend {
}
try {
await initLbug(repoId, handle.lbugPath);
this.initializedRepos.add(repoId);
await initLbug(poolKey, repo.lbugPath);
this.initializedRepos.add(poolKey);
} catch (err: any) {
// If lock error, mark as not initialized so next call retries
this.initializedRepos.delete(repoId);
this.initializedRepos.delete(poolKey);
throw err;
}
}
@ -930,7 +1020,7 @@ export class LocalBackend {
return { error: 'query parameter is required and cannot be empty.' };
}
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const processLimit = params.limit || 5;
const maxSymbolsPerProcess = params.max_symbols || 10;
@ -1027,7 +1117,7 @@ export class LocalBackend {
let processRows: any[] = [];
try {
processRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
@ -1043,7 +1133,7 @@ export class LocalBackend {
let module: string | undefined;
try {
const cohesionRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $nodeId})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
@ -1064,7 +1154,7 @@ export class LocalBackend {
if (includeContent) {
try {
const contentRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $nodeId})
RETURN n.content AS content
@ -1208,7 +1298,7 @@ export class LocalBackend {
}
let ftsResponse;
try {
ftsResponse = await searchFTSFromLbug(query, limit, repo.id);
ftsResponse = await searchFTSFromLbug(query, limit, repo.lbugPath);
} catch (err: any) {
logger.error(
{ err: err.message },
@ -1233,7 +1323,7 @@ export class LocalBackend {
const nodeIds = bm25Result.nodeIds?.length ? bm25Result.nodeIds : null;
const symbols = nodeIds
? await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)
WHERE n.id IN $nodeIds
@ -1242,7 +1332,7 @@ export class LocalBackend {
{ nodeIds },
)
: await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)
WHERE n.filePath = $filePath
@ -1294,7 +1384,7 @@ export class LocalBackend {
try {
// Check if embedding table exists before loading the model (avoids heavy model init when embeddings are off)
const tableCheck = await executeQuery(
repo.id,
repo.lbugPath,
`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN COUNT(*) AS cnt LIMIT 1`,
);
if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0) return [];
@ -1322,7 +1412,7 @@ export class LocalBackend {
ORDER BY distance
`;
const embResults = await executeQuery(repo.id, vectorQuery);
const embResults = await executeQuery(repo.lbugPath, vectorQuery);
return embResults.map((row) => ({
nodeId: row.nodeId ?? row[0],
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
@ -1351,7 +1441,7 @@ export class LocalBackend {
if (embeddingCount > exactLimit) return [];
const rows = await executeQuery(
repo.id,
repo.lbugPath,
`
MATCH (e:${EMBEDDING_TABLE_NAME})
RETURN e.nodeId AS nodeId, e.chunkIndex AS chunkIndex,
@ -1395,7 +1485,7 @@ export class LocalBackend {
? `MATCH (n:File {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath`
: `MATCH (n:\`${label}\` {id: $nodeId}) RETURN n.name AS name, n.filePath AS filePath`;
const nodeRows = await executeParameterized(repo.id, nodeQuery, { nodeId });
const nodeRows = await executeParameterized(repo.lbugPath, nodeQuery, { nodeId });
if (nodeRows.length > 0) {
const nodeRow = nodeRows[0];
results.push({
@ -1431,9 +1521,9 @@ export class LocalBackend {
repo: RepoHandle,
request: { query: string; params?: Record<string, unknown> },
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
if (!isLbugReady(repo.id)) {
if (!isLbugReady(repo.lbugPath)) {
return { error: 'LadybugDB not ready. Index may be corrupted.' };
}
if (request.params !== undefined && !isValidQueryParams(request.params)) {
@ -1443,7 +1533,7 @@ export class LocalBackend {
}
try {
const result = await executeParameterized(repo.id, request.query, request.params ?? {});
const result = await executeParameterized(repo.lbugPath, request.query, request.params ?? {});
return result;
} catch (err: any) {
const msg = err.message || 'Query failed';
@ -1549,7 +1639,7 @@ export class LocalBackend {
repo: RepoHandle,
params: { showClusters?: boolean; showProcesses?: boolean; limit?: number },
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const limit = params.limit || 20;
const result: any = {
@ -1565,7 +1655,7 @@ export class LocalBackend {
// Fetch more raw communities than the display limit so aggregation has enough data
const rawLimit = Math.max(limit * 5, 200);
const clusters = await executeQuery(
repo.id,
repo.lbugPath,
`
MATCH (c:Community)
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
@ -1589,7 +1679,7 @@ export class LocalBackend {
if (params.showProcesses !== false) {
try {
const processes = await executeQuery(
repo.id,
repo.lbugPath,
`
MATCH (p:Process)
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
@ -1633,7 +1723,7 @@ export class LocalBackend {
if (ids.length === 0) return;
try {
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n:\`Class\`) WHERE n.id IN $ids RETURN n.id AS id, 'Class' AS label
UNION ALL
@ -1756,7 +1846,7 @@ export class LocalBackend {
// Direct UID — zero-ambiguity path.
if (uid) {
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`MATCH (n {id: $uid}) RETURN ${selectClause} LIMIT 1`,
{ uid },
);
@ -1795,7 +1885,7 @@ export class LocalBackend {
// LIMIT 20 (was 10) — scoring is the point now, so give the ranker
// headroom instead of arbitrary truncation.
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`MATCH (n) ${whereClause} RETURN ${selectClause} LIMIT 20`,
queryParams,
);
@ -1835,7 +1925,7 @@ export class LocalBackend {
const candidateIds = normalized.map((s) => s.id).filter(Boolean);
for (const label of ['Class', 'Interface']) {
const labelRows = await executeParameterized(
repo.id,
repo.lbugPath,
`MATCH (n:\`${label}\`) WHERE n.id IN $candidateIds RETURN n.id AS id LIMIT 1`,
{ candidateIds },
).catch(() => []);
@ -1938,7 +2028,7 @@ export class LocalBackend {
include_content?: boolean;
},
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const { name, uid, file_path, kind, include_content } = params;
@ -1978,7 +2068,7 @@ export class LocalBackend {
// Categorized incoming refs
const incomingRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
@ -2003,7 +2093,7 @@ export class LocalBackend {
try {
// Single UNION query instead of two serial round-trips.
const typeCheck = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n:Class) WHERE n.id = $symId RETURN 'Class' AS label LIMIT 1
UNION ALL
@ -2025,7 +2115,7 @@ export class LocalBackend {
const [ctorIncoming, fileIncoming, typedPropertyIncoming, typedProperties] =
await Promise.all([
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[hm:CodeRelation]->(ctor:Constructor)
WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
@ -2037,7 +2127,7 @@ export class LocalBackend {
{ symId },
),
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (f:File)-[rel:CodeRelation]->(n)
WHERE n.id = $symId AND rel.type = 'DEFINES'
@ -2049,7 +2139,7 @@ export class LocalBackend {
{ symId },
),
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (p:\`Property\`)
WHERE p.declaredType = $name
@ -2067,7 +2157,7 @@ export class LocalBackend {
},
),
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (p:\`Property\`)
WHERE p.declaredType = $name
@ -2106,7 +2196,7 @@ export class LocalBackend {
// Categorized outgoing refs
const outgoingRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
@ -2120,7 +2210,7 @@ export class LocalBackend {
let processRows: any[] = [];
try {
processRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $symId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
@ -2156,7 +2246,7 @@ export class LocalBackend {
if (isMethodLike) {
try {
const metaRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n {id: $symId})
RETURN n.visibility AS visibility, n.isStatic AS isStatic, n.isAbstract AS isAbstract,
@ -2226,7 +2316,7 @@ export class LocalBackend {
repo: RepoHandle,
params: { name: string; type: 'symbol' | 'cluster' | 'process' },
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const { name, type } = params;
if (type === 'symbol') {
@ -2235,7 +2325,7 @@ export class LocalBackend {
if (type === 'cluster') {
const clusters = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (c:Community)
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
@ -2262,7 +2352,7 @@ export class LocalBackend {
}
const members = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
@ -2291,7 +2381,7 @@ export class LocalBackend {
if (type === 'process') {
const processes = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (p:Process)
WHERE p.label = $processName OR p.heuristicLabel = $processName
@ -2305,7 +2395,7 @@ export class LocalBackend {
const proc = processes[0];
const procId = proc.id || proc[0];
const steps = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
@ -2346,7 +2436,7 @@ export class LocalBackend {
worktree?: string;
},
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const scope = params.scope || 'unstaged';
const { execFileSync } = await import('child_process');
@ -2465,7 +2555,7 @@ export class LocalBackend {
`;
try {
const rows = await executeParameterized(repo.id, symbolQuery, queryParams);
const rows = await executeParameterized(repo.lbugPath, symbolQuery, queryParams);
for (const sym of rows) {
changedSymbols.push({
id: sym.id || sym[0],
@ -2487,7 +2577,7 @@ export class LocalBackend {
const symNameById = new Map(changedSymbols.map((s) => [s.id, s.name]));
try {
const procs = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
WHERE n.id IN $ids
@ -2555,7 +2645,7 @@ export class LocalBackend {
dry_run?: boolean;
},
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const { new_name, file_path } = params;
const dry_run = params.dry_run ?? true;
@ -2783,7 +2873,7 @@ export class LocalBackend {
}
private async _impactImpl(repo: RepoHandle, params: ImpactParams): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const { target, direction } = params;
const maxDepth = params.maxDepth || 3;
@ -2948,7 +3038,7 @@ export class LocalBackend {
// Run both seed queries in parallel — they are independent.
const [ctorRows, fileRows] = await Promise.all([
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[hm:CodeRelation]->(c:Constructor)
WHERE n.id = $symId AND hm.type = 'HAS_METHOD'
@ -2959,7 +3049,7 @@ export class LocalBackend {
// Restrict to DEFINES edges only — other File->Class edge types (if
// any) should not be treated as the owning file relationship.
executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (f:File)-[rel:CodeRelation]->(n)
WHERE n.id = $symId AND rel.type = 'DEFINES'
@ -2985,7 +3075,7 @@ export class LocalBackend {
}
const typedPropertyRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (p:\`Property\`)
WHERE p.declaredType = $name
@ -3023,7 +3113,7 @@ export class LocalBackend {
: `MATCH (n)-[r:CodeRelation]->(callee) WHERE n.id IN $frontierIds AND r.type IN $relTypes${confidenceFilter} RETURN n.id AS sourceId, callee.id AS id, callee.name AS name, labels(callee)[0] AS type, callee.filePath AS filePath, r.type AS relType, r.confidence AS confidence`;
try {
const related = await executeParameterized(repo.id, query, {
const related = await executeParameterized(repo.lbugPath, query, {
frontierIds: frontier,
relTypes: relationTypes,
...(safeMinConfidence > 0 ? { minConfidence: safeMinConfidence } : {}),
@ -3130,7 +3220,7 @@ export class LocalBackend {
try {
// Use parameterized list to avoid building long query strings
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
WHERE s.id IN $ids
@ -3204,7 +3294,7 @@ export class LocalBackend {
const pIds = Array.from(processesMissingMinStep);
const allImpactedIds = impacted.map((it) => String(it.id ?? ''));
const missingRows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
WHERE p.id IN $pIds AND s.id IN $ids
@ -3264,7 +3354,7 @@ export class LocalBackend {
if (!idsChunk || idsChunk.length === 0) return;
try {
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
WHERE s.id IN $ids
@ -3297,7 +3387,7 @@ export class LocalBackend {
if (!idsChunk || idsChunk.length === 0) return;
try {
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
WHERE s.id IN $ids
@ -3435,7 +3525,7 @@ export class LocalBackend {
const chunkIds = pageIdArr.slice(i, i + CHUNK_SIZE);
try {
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
WHERE s.id IN $ids
@ -3508,22 +3598,24 @@ export class LocalBackend {
// scope — the caller's Promise.race against the same signal
// resolves the await regardless of how long this body runs.
if (opts.signal?.aborted) return null;
let repo: RepoHandle | undefined;
try {
await this.refreshRepos();
await this.ensureInitialized(repoId);
// Fetch the resolved handle BEFORE init and pass it through, so a
// concurrent refresh can't remap the id to a different clone (#2067).
repo = this.repos.get(repoId);
if (repo) await this.ensureInitialized(repo);
} catch {
return null;
}
const repo = this.repos.get(repoId);
if (!repo) return null;
if (!repo) return null; // unknown repo → null (preserves contract)
const dir: 'upstream' | 'downstream' = direction === 'downstream' ? 'downstream' : 'upstream';
let rows: any[];
try {
rows = await executeParameterized(
repoId,
repo.lbugPath, // pool keyed by the resolved clone's path, not the id
`MATCH (n) WHERE n.id = $uid
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, labels(n)[0] AS type
LIMIT 1`,
@ -3893,11 +3985,11 @@ export class LocalBackend {
}
private async routeMap(repo: RepoHandle, params: { route?: string }): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const routeFilter = params.route ? `AND n.name CONTAINS $route` : '';
const queryParams = params.route ? { route: params.route } : {};
const routes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
const routes = await this.fetchRoutesWithConsumers(repo.lbugPath, routeFilter, queryParams);
if (routes.length === 0) {
return {
@ -3910,7 +4002,7 @@ export class LocalBackend {
}
const flowMap = await this.fetchLinkedFlowsBatch(
repo.id,
repo.lbugPath,
routes.map((r) => r.id),
);
@ -3927,11 +4019,11 @@ export class LocalBackend {
}
private async shapeCheck(repo: RepoHandle, params: { route?: string }): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const routeFilter = params.route ? `AND n.name CONTAINS $route` : '';
const queryParams = params.route ? { route: params.route } : {};
const allRoutes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
const allRoutes = await this.fetchRoutesWithConsumers(repo.lbugPath, routeFilter, queryParams);
const results = allRoutes
.filter(
@ -4009,13 +4101,13 @@ export class LocalBackend {
}
private async toolMap(repo: RepoHandle, params: { tool?: string }): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const toolFilter = params.tool ? `AND n.name CONTAINS $tool` : '';
const queryParams = params.tool ? { tool: params.tool } : {};
const rows = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n:Tool)
WHERE n.id STARTS WITH 'Tool:' ${toolFilter}
@ -4033,7 +4125,7 @@ export class LocalBackend {
}
const toolIds = rows.map((r: any) => r.id ?? r[0]);
const flowMap = await this.fetchLinkedFlowsBatch(repo.id, toolIds);
const flowMap = await this.fetchLinkedFlowsBatch(repo.lbugPath, toolIds);
return {
tools: rows.map((r: any) => {
@ -4053,7 +4145,7 @@ export class LocalBackend {
repo: RepoHandle,
params: { route?: string; file?: string },
): Promise<any> {
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
if (!params.route && !params.file) {
return { error: 'Either "route" or "file" parameter is required.' };
@ -4071,7 +4163,7 @@ export class LocalBackend {
queryParams.file = params.file;
}
const routes = await this.fetchRoutesWithConsumers(repo.id, routeFilter, queryParams);
const routes = await this.fetchRoutesWithConsumers(repo.lbugPath, routeFilter, queryParams);
if (routes.length === 0) {
const target = params.route || params.file;
@ -4079,7 +4171,7 @@ export class LocalBackend {
}
const flowMap = await this.fetchLinkedFlowsBatch(
repo.id,
repo.lbugPath,
routes.map((r) => r.id),
);
@ -4205,12 +4297,12 @@ export class LocalBackend {
*/
async queryClusters(repoName?: string, limit = 100): Promise<{ clusters: any[] }> {
const repo = await this.resolveRepo(repoName);
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
try {
const rawLimit = Math.max(limit * 5, 200);
const clusters = await executeQuery(
repo.id,
repo.lbugPath,
`
MATCH (c:Community)
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
@ -4237,11 +4329,11 @@ export class LocalBackend {
*/
async queryProcesses(repoName?: string, limit = 50): Promise<{ processes: any[] }> {
const repo = await this.resolveRepo(repoName);
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
try {
const processes = await executeQuery(
repo.id,
repo.lbugPath,
`
MATCH (p:Process)
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
@ -4269,10 +4361,10 @@ export class LocalBackend {
*/
async queryClusterDetail(name: string, repoName?: string): Promise<any> {
const repo = await this.resolveRepo(repoName);
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const clusters = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (c:Community)
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
@ -4299,7 +4391,7 @@ export class LocalBackend {
}
const members = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
@ -4332,10 +4424,10 @@ export class LocalBackend {
*/
async queryProcessDetail(name: string, repoName?: string): Promise<any> {
const repo = await this.resolveRepo(repoName);
await this.ensureInitialized(repo.id);
await this.ensureInitialized(repo);
const processes = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (p:Process)
WHERE p.label = $processName OR p.heuristicLabel = $processName
@ -4349,7 +4441,7 @@ export class LocalBackend {
const proc = processes[0];
const procId = proc.id || proc[0];
const steps = await executeParameterized(
repo.id,
repo.lbugPath,
`
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step

View file

@ -292,7 +292,7 @@ async function getReposResource(backend: LocalBackend): Promise<string> {
if (repos.length > 1) {
lines.push('');
lines.push('# Multiple repos indexed. Use repo parameter in tool calls:');
lines.push(`# gitnexus_search({query: "auth", repo: "${repos[0].name}"})`);
lines.push(`# query({query: "auth", repo: "${repos[0].name}"})`);
}
return lines.join('\n');
@ -382,7 +382,7 @@ async function getClustersResource(backend: LocalBackend, repoName?: string): Pr
if (result.clusters.length > displayLimit) {
lines.push(
`\n# Showing top ${displayLimit} of ${result.clusters.length} modules. Use gitnexus_query for deeper search.`,
`\n# Showing top ${displayLimit} of ${result.clusters.length} modules. Use the query tool for deeper search.`,
);
}
@ -416,7 +416,7 @@ async function getProcessesResource(backend: LocalBackend, repoName?: string): P
if (result.processes.length > displayLimit) {
lines.push(
`\n# Showing top ${displayLimit} of ${result.processes.length} processes. Use gitnexus_query for deeper search.`,
`\n# Showing top ${displayLimit} of ${result.processes.length} processes. Use the query tool for deeper search.`,
);
}

View file

@ -912,6 +912,18 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust
expect(content).toContain('base_ref: "main"');
});
it('references MCP tools by their registered (unprefixed) names (#2059)', () => {
const content = generateGitNexusContent('P', { nodes: 50, edges: 100, processes: 5 });
// The server registers tools without a `gitnexus_` prefix (see mcp/tools.ts);
// generated instructions must use the exact callable names or agents call a
// tool that does not exist.
expect(content).not.toMatch(/gitnexus_(impact|query|context|detect_changes|rename|cypher)/);
expect(content).toContain('impact({target: "symbolName", direction: "upstream"})');
expect(content).toContain('detect_changes()');
expect(content).toContain('query({query: "concept"})');
expect(content).toContain('context({name: "symbolName"})');
});
it('JSON-escapes a markdown/quote-bearing branch so it cannot break the code span (#243)', () => {
// A branch name with a double-quote must be JSON-escaped, not concatenated
// raw, so it stays inside the inline code span.
@ -959,7 +971,7 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
- run \`gitnexus_detect_changes({scope: "compare", base_ref: "main"})\`.
- run \`detect_changes({scope: "compare", base_ref: "main"})\`.
| Task | Read this skill file |
|------|---------------------|

View file

@ -178,6 +178,39 @@ function makeSharedPrefixFixture(nameA: string, nameB: string) {
};
}
// Mirrors the legacy `repoId()` suffix that #2054 replaced for genuine
// collisions: base64url is an *encoding*, not a hash, so paths sharing a long
// prefix produce the same sliced suffix. Used by the #2054 tests to assert the
// collision precondition actually holds (so the regression isn't vacuous).
function legacyPathSuffix(p: string): string {
return Buffer.from(p).toString('base64url').slice(0, REPO_ID_HASH_LENGTH).toLowerCase();
}
/**
* Build N sibling clones of one remote under a SINGLE parent directory, named
* REPO, REPO_2, …, REPO_N. All clones share the remote-inferred registry name
* ("REPO") and the same remoteUrl — this is the real-world #2054 setup. Because
* the clones live under one parent, their absolute paths share a long common
* prefix, which is exactly what made the 6-char base64url suffix collide.
* (mkdtemp'ing each clone separately would NOT reproduce the bug — the random
* suffixes diverge in the first few bytes.)
*/
function makeSiblingClonesFixture(count: number, remoteUrl = 'git@github.com:MYCOMPANY/REPO.git') {
const parent = mkdtempSync(path.join(os.tmpdir(), 'gnx-2054-'));
duplicateFixtureDirs.push(parent);
const folders = Array.from({ length: count }, (_, i) => (i === 0 ? 'REPO' : `REPO_${i + 1}`));
const dirs: string[] = [];
const entries = folders.map((folder) => {
const dir = path.join(parent, folder);
const storagePath = path.join(dir, '.gitnexus');
mkdirSync(path.join(storagePath, 'lbug'), { recursive: true });
writeFileSync(path.join(storagePath, 'meta.json'), '{}');
dirs.push(dir);
return { ...MOCK_REPO_ENTRY, name: 'REPO', path: dir, storagePath, remoteUrl };
});
return { parent, dirs, entries };
}
// ─── LocalBackend lifecycle ──────────────────────────────────────────
describe('LocalBackend.init', () => {
@ -1333,6 +1366,293 @@ describe('LocalBackend.resolveRepo', () => {
});
});
// ─── repo-id collisions (sibling clones) ────────────────────────────
describe('LocalBackend repo-id collisions (#2054)', () => {
let backend: LocalBackend;
beforeEach(() => {
vi.clearAllMocks();
(getGitRoot as any).mockReturnValue(null);
backend = new LocalBackend();
});
afterEach(() => {
for (const dir of duplicateFixtureDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
it('lists all four sibling clones that share a name and remote (#2054)', async () => {
const { dirs, entries } = makeSiblingClonesFixture(4);
(listRegisteredRepos as any).mockResolvedValue(entries);
// Precondition: the historical 6-char base64url suffixes really do collide
// for these sibling paths — otherwise this test would not exercise the bug.
expect(legacyPathSuffix(dirs[1])).toBe(legacyPathSuffix(dirs[2]));
expect(legacyPathSuffix(dirs[2])).toBe(legacyPathSuffix(dirs[3]));
expect(await backend.init()).toBe(true);
const listed = await backend.callTool('list_repos', {});
expect(listed).toHaveLength(4);
// Every distinct on-disk clone survives exactly once — no silent overwrite.
const listedPaths = listed.map((r: any) => path.resolve(r.path)).sort();
expect(listedPaths).toEqual(dirs.map((d) => path.resolve(d)).sort());
expect(new Set(listedPaths).size).toBe(4);
// The shared remoteUrl must NOT collapse the entries; instead each entry
// reports the other three as siblings (existing list_repos contract).
for (const entry of listed) {
expect(entry.remoteUrl).toBe('git@github.com:MYCOMPANY/REPO.git');
expect(entry.siblings).toHaveLength(3);
}
// Every clone is addressable by its absolute path.
for (const dir of dirs) {
const resolved = await backend.resolveRepo(dir);
expect(resolved.repoPath).toBe(dir);
}
// Re-running list_repos (which re-reads the registry) is idempotent.
const again = await backend.callTool('list_repos', {});
expect(again).toHaveLength(4);
});
it('assigns distinct, resolvable generated ids past the first legacy collision (#2054)', async () => {
const { dirs, entries } = makeSiblingClonesFixture(4);
(listRegisteredRepos as any).mockResolvedValue(entries);
await backend.init();
// Resolve each clone by path, collect its in-memory id.
const handles = await Promise.all(dirs.map((d) => backend.resolveRepo(d)));
const ids = handles.map((h) => h.id);
// Ids are unique across all four clones.
expect(new Set(ids).size).toBe(4);
// First clone keeps the bare name; the rest are name-prefixed generated ids.
expect(ids[0]).toBe('repo');
for (const id of ids.slice(1)) expect(id.startsWith('repo-')).toBe(true);
// Clones that collided on the legacy suffix fall back to a content hash —
// i.e. they are NOT addressable by the (colliding) legacy id, but ARE
// addressable by whatever stable id they actually hold.
const collidedLegacy = `repo-${legacyPathSuffix(dirs[2])}`;
expect(handles[2].id).not.toBe(collidedLegacy);
expect(handles[3].id).not.toBe(handles[2].id);
// Each *suffixed* generated id resolves back to its own clone. The bare
// "repo" id is intentionally shadowed by the shared repo *name* (the #1658
// name tier runs before the id tier), so the first clone is addressed by
// path instead — covered by the headline test.
for (const h of handles.slice(1)) {
const viaId = await backend.resolveRepo(h.id);
expect(viaId.repoPath).toBe(h.repoPath);
}
});
it('keeps each clone’s generated id stable across a registry reorder (#2067)', async () => {
// Ids are assigned over a path-sorted view, so the same resolved path always
// gets the same id regardless of registry order — a memorized hashed id
// can't drift to a different clone after a reorder.
const { dirs, entries } = makeSiblingClonesFixture(4);
(listRegisteredRepos as any).mockResolvedValue(entries);
await backend.init();
const before: Record<string, string> = {};
for (const d of dirs) before[d] = (await backend.resolveRepo(d)).id;
// Reverse the registry order and refresh.
(listRegisteredRepos as any).mockResolvedValue([...entries].reverse());
await backend.callTool('list_repos', {});
for (const d of dirs) {
expect((await backend.resolveRepo(d)).id).toBe(before[d]); // same path → same id
}
});
it('refresh stability: reorder, remove-one, and re-add never drop a different clone (#2054)', async () => {
const { dirs, entries } = makeSiblingClonesFixture(4);
const listedPaths = async () =>
(await backend.callTool('list_repos', {})).map((r: any) => path.resolve(r.path)).sort();
const allPaths = dirs.map((d) => path.resolve(d)).sort();
(listRegisteredRepos as any).mockResolvedValue(entries);
await backend.init();
expect(await listedPaths()).toEqual(allPaths);
// Reordering the registry must not silently lose a clone. (Under path-sorted
// assignment a reorder is a no-op for id assignment; id stability across
// reorder is asserted separately above. This step remains a set-survival
// guard.)
(listRegisteredRepos as any).mockResolvedValue([...entries].reverse());
expect(await listedPaths()).toEqual(allPaths);
// Removing one entry prunes only that entry.
(listRegisteredRepos as any).mockResolvedValue(entries.slice(0, 3));
expect(await listedPaths()).toEqual(
dirs
.slice(0, 3)
.map((d) => path.resolve(d))
.sort(),
);
// Re-adding it restores it without replacing another clone.
(listRegisteredRepos as any).mockResolvedValue(entries);
expect(await listedPaths()).toEqual(allPaths);
});
it('gives two same-name clones independent pools and never evicts on id reassignment (#2067)', async () => {
// The pool (and the init/staleness/reinit maps) are keyed by the immutable
// lbugPath, so two clones that transiently share a name-derived id get
// SEPARATE pool entries — neither can be served the other's database — and a
// pure id reassignment (path still registered) needs no pool eviction.
const parent = mkdtempSync(path.join(os.tmpdir(), 'gnx-remap-'));
duplicateFixtureDirs.push(parent);
const a = path.join(parent, 'A'); // 'A' sorts before 'B'
const b = path.join(parent, 'B');
const lbug = (dir: string) => path.join(dir, '.gitnexus', 'lbug');
const mk = (dir: string) => {
mkdirSync(lbug(dir), { recursive: true });
writeFileSync(path.join(dir, '.gitnexus', 'meta.json'), '{}');
return {
...MOCK_REPO_ENTRY,
name: 'dup',
path: dir,
storagePath: path.join(dir, '.gitnexus'),
};
};
const entryA = mk(a);
const entryB = mk(b);
// Start with only B → B owns the bare "dup" id; resolve it.
(listRegisteredRepos as any).mockResolvedValue([entryB]);
await backend.init();
const handleB = await backend.resolveRepo(b);
expect(handleB.id).toBe('dup');
// Add A (sorts before B) → the bare "dup" id is reassigned to A.
(closeLbug as any).mockClear();
(listRegisteredRepos as any).mockResolvedValue([entryB, entryA]);
await backend.callTool('list_repos', {});
const handleA = await backend.resolveRepo(a);
expect(handleA.id).toBe('dup'); // A now owns the bare id
expect((await backend.resolveRepo(b)).id).not.toBe('dup'); // B moved to a suffix
// Reassigning the id evicts nothing — both paths are still registered.
expect(closeLbug).not.toHaveBeenCalled();
// Each clone initializes its OWN pool entry, keyed by its own lbugPath — no
// cross-serving even though they shared the "dup" id.
(initLbug as any).mockClear();
await (backend as any).ensureInitialized(handleA);
await (backend as any).ensureInitialized(handleB);
expect(initLbug).toHaveBeenCalledWith(lbug(a), lbug(a));
expect(initLbug).toHaveBeenCalledWith(lbug(b), lbug(b));
});
it('releases the pooled connection when a repo path leaves the registry (#2054)', async () => {
// When a clone's path is unregistered its pooled LadybugDB connection must
// be released. The pool is keyed by lbugPath, so eviction targets the path.
const parent = mkdtempSync(path.join(os.tmpdir(), 'gnx-vanish-'));
duplicateFixtureDirs.push(parent);
const dir = path.join(parent, 'solo');
const lbugPath = path.join(dir, '.gitnexus', 'lbug');
mkdirSync(lbugPath, { recursive: true });
writeFileSync(path.join(dir, '.gitnexus', 'meta.json'), '{}');
const entry = {
...MOCK_REPO_ENTRY,
name: 'solo',
path: dir,
storagePath: path.join(dir, '.gitnexus'),
};
(listRegisteredRepos as any).mockResolvedValue([entry]);
await backend.init();
expect((await backend.resolveRepo(dir)).id).toBe('solo');
// Registry now empty → the clone's path vanishes on refresh.
(closeLbug as any).mockClear();
(listRegisteredRepos as any).mockResolvedValue([]);
await backend.callTool('list_repos', {});
expect(closeLbug).toHaveBeenCalledWith(lbugPath);
});
it('initializes the resolved clone, not a clone the id was remapped to mid-call (#2067)', async () => {
// ensureInitialized takes the resolved RepoHandle, so even if a concurrent
// refresh remaps the (floating) bare id to a different clone between resolve
// and init, it opens the clone the caller actually resolved — not whatever
// the id now points at. Pre-fix (by-id re-derivation) it opened the remapped
// clone's database.
const parent = mkdtempSync(path.join(os.tmpdir(), 'gnx-race-'));
duplicateFixtureDirs.push(parent);
const a = path.join(parent, 'A'); // 'A' sorts before 'B'
const b = path.join(parent, 'B');
const mk = (dir: string) => {
mkdirSync(path.join(dir, '.gitnexus', 'lbug'), { recursive: true });
writeFileSync(path.join(dir, '.gitnexus', 'meta.json'), '{}');
return {
...MOCK_REPO_ENTRY,
name: 'dup',
path: dir,
storagePath: path.join(dir, '.gitnexus'),
};
};
const entryA = mk(a);
const entryB = mk(b);
// Only B registered → B owns the bare "dup" id; resolve it.
(listRegisteredRepos as any).mockResolvedValue([entryB]);
await backend.init();
const resolvedB = await backend.resolveRepo(b);
expect(resolvedB.id).toBe('dup');
// Concurrent refresh adds A (sorts first) → the bare "dup" id now maps to A.
(listRegisteredRepos as any).mockResolvedValue([entryB, entryA]);
await backend.callTool('list_repos', {});
expect((await backend.resolveRepo(a)).id).toBe('dup'); // id remapped to A
// Initialize with the handle resolved BEFORE the remap → must open B's path
// (pool keyed by B's lbugPath), never A's.
(initLbug as any).mockClear();
await (backend as any).ensureInitialized(resolvedB);
const lbug = (dir: string) => path.join(dir, '.gitnexus', 'lbug');
expect(initLbug).toHaveBeenCalledWith(lbug(b), lbug(b));
expect(initLbug).not.toHaveBeenCalledWith(lbug(a), lbug(a));
});
it('handles more than four sibling clones — all listed once and resolvable (#2067)', async () => {
const { dirs, entries } = makeSiblingClonesFixture(6);
(listRegisteredRepos as any).mockResolvedValue(entries);
await backend.init();
const listed = await backend.callTool('list_repos', {});
expect(listed).toHaveLength(6);
// All six ids are distinct (clones 3–6 exercise the sha256 fallback tier).
const ids = await Promise.all(dirs.map(async (d) => (await backend.resolveRepo(d)).id));
expect(new Set(ids).size).toBe(6);
for (const d of dirs) expect((await backend.resolveRepo(d)).repoPath).toBe(d);
});
it('lists same-name clones with no remoteUrl without grouping or collapse (#2067)', async () => {
const { dirs, entries } = makeSiblingClonesFixture(2);
// Strip remoteUrl — same name, no remote fingerprint.
const noRemote = entries.map((e) => ({ ...e, remoteUrl: undefined }));
(listRegisteredRepos as any).mockResolvedValue(noRemote);
await backend.init();
const listed = await backend.callTool('list_repos', {});
expect(listed).toHaveLength(2); // both present, not collapsed
for (const e of listed) {
expect(e.remoteUrl).toBeUndefined();
expect(e.siblings).toBeUndefined(); // no remote → no sibling grouping
}
for (const d of dirs) expect((await backend.resolveRepo(d)).repoPath).toBe(d);
});
});
// ─── getContext ──────────────────────────────────────────────────────
describe('LocalBackend.getContext', () => {

View file

@ -386,5 +386,9 @@ describe('readResource', () => {
const result = await readResource('gitnexus://repos', backend);
expect(result).toContain('Multiple repos indexed');
expect(result).toContain('repo parameter');
// The example must use a registered tool name, not the unregistered
// `gitnexus_search` / `gitnexus_*` prefix (#2059).
expect(result).toContain('query({query: "auth"');
expect(result).not.toMatch(/gitnexus_/);
});
});

View file

@ -624,6 +624,35 @@ describe('generateSkillFiles — file output', () => {
expect(content).toContain('description:');
});
/**
* The "How to Explore" section must reference MCP tools by their registered
* (unprefixed) names — the server registers `context`/`query`, not
* `gitnexus_context`/`gitnexus_query`, so the prefixed form points agents at
* tools that do not exist (#2059).
*/
it('references MCP tools by their registered (unprefixed) names (#2059)', async () => {
const { graph, communities, memberships } = twoCommSetup();
await generateSkillFiles(
tmpDir,
'TestProject',
buildPipelineResult({
graph,
repoPath: tmpDir,
communities,
memberships,
}),
);
const content = await fs.readFile(
path.join(tmpDir, '.claude', 'skills', 'generated', 'alpha', 'SKILL.md'),
'utf-8',
);
expect(content).not.toMatch(/gitnexus_(context|query|impact|detect_changes|rename|cypher)/);
expect(content).toContain('context({name:');
expect(content).toContain('query({query:');
});
/**
* A community with exported symbols, processes, and cross-community
* CALLS edges should have all optional sections rendered.