diff --git a/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md index 937b5e2a4..9834f94b7 100644 --- a/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-debugging/SKILL.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. gitnexus_query({query: ""}) → Find related execution flows -2. gitnexus_context({name: ""}) → See callers/callees/processes +1. query({query: ""}) → Find related execution flows +2. context({name: ""}) → 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 diff --git a/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md index 2dcf7b578..ccf684c28 100644 --- a/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-exploring/SKILL.md @@ -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: ""}) → Find related execution flows -4. gitnexus_context({name: ""}) → Deep dive on specific symbol +3. query({query: ""}) → Find related execution flows +4. context({name: ""}) → 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 diff --git a/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md b/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md index 7206ca506..45eb7ce87 100644 --- a/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md @@ -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) diff --git a/.claude/skills/gitnexus/gitnexus-pr-review/SKILL.md b/.claude/skills/gitnexus/gitnexus-pr-review/SKILL.md index 319c063f9..9f1d362e5 100644 --- a/.claude/skills/gitnexus/gitnexus-pr-review/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-pr-review/SKILL.md @@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what ``` 1. gh pr diff → 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: "", direction: "upstream"}) → Blast radius per change -4. gitnexus_context({name: ""}) → Understand callers/callees + impact({target: "", direction: "upstream"}) → Blast radius per change +4. context({name: ""}) → 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: diff --git a/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md index c749eb384..e13c04e14 100644 --- a/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md @@ -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 ``` diff --git a/AGENTS.md b/AGENTS.md index 1e31004e4..d4b09c8ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index bbb991589..f2bf1e487 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md index 937b5e2a4..9834f94b7 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-debugging/SKILL.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. gitnexus_query({query: ""}) → Find related execution flows -2. gitnexus_context({name: ""}) → See callers/callees/processes +1. query({query: ""}) → Find related execution flows +2. context({name: ""}) → 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 diff --git a/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md index 2dcf7b578..ccf684c28 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-exploring/SKILL.md @@ -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: ""}) → Find related execution flows -4. gitnexus_context({name: ""}) → Deep dive on specific symbol +3. query({query: ""}) → Find related execution flows +4. context({name: ""}) → 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 diff --git a/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md index 7206ca506..45eb7ce87 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-impact-analysis/SKILL.md @@ -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) diff --git a/gitnexus-claude-plugin/skills/gitnexus-pr-review/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-pr-review/SKILL.md index 319c063f9..9f1d362e5 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-pr-review/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-pr-review/SKILL.md @@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what ``` 1. gh pr diff → 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: "", direction: "upstream"}) → Blast radius per change -4. gitnexus_context({name: ""}) → Understand callers/callees + impact({target: "", direction: "upstream"}) → Blast radius per change +4. context({name: ""}) → 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: diff --git a/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md b/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md index c749eb384..e13c04e14 100644 --- a/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md +++ b/gitnexus-claude-plugin/skills/gitnexus-refactoring/SKILL.md @@ -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 ``` diff --git a/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md index a7e250647..a88b76430 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-debugging/SKILL.md @@ -15,10 +15,10 @@ description: Trace bugs through call chains using knowledge graph ## Workflow ``` -1. gitnexus_query({query: ""}) → Find related execution flows -2. gitnexus_context({name: ""}) → See callers/callees/processes +1. query({query: ""}) → Find related execution flows +2. context({name: ""}) → 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 diff --git a/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md index 4549505e5..73df1353a 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-exploring/SKILL.md @@ -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: ""}) → Find related execution flows -4. gitnexus_context({name: ""}) → Deep dive on specific symbol +3. query({query: ""}) → Find related execution flows +4. context({name: ""}) → 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 diff --git a/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md index 0733b09ac..139b897e4 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-impact-analysis/SKILL.md @@ -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) diff --git a/gitnexus-cursor-integration/skills/gitnexus-pr-review/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-pr-review/SKILL.md index 319c063f9..9f1d362e5 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-pr-review/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-pr-review/SKILL.md @@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what ``` 1. gh pr diff → 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: "", direction: "upstream"}) → Blast radius per change -4. gitnexus_context({name: ""}) → Understand callers/callees + impact({target: "", direction: "upstream"}) → Blast radius per change +4. context({name: ""}) → 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: diff --git a/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md index a49b58be4..76c9d3351 100644 --- a/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md +++ b/gitnexus-cursor-integration/skills/gitnexus-refactoring/SKILL.md @@ -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 ``` diff --git a/gitnexus/skills/gitnexus-debugging.md b/gitnexus/skills/gitnexus-debugging.md index 937b5e2a4..9834f94b7 100644 --- a/gitnexus/skills/gitnexus-debugging.md +++ b/gitnexus/skills/gitnexus-debugging.md @@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking ## Workflow ``` -1. gitnexus_query({query: ""}) → Find related execution flows -2. gitnexus_context({name: ""}) → See callers/callees/processes +1. query({query: ""}) → Find related execution flows +2. context({name: ""}) → 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 diff --git a/gitnexus/skills/gitnexus-exploring.md b/gitnexus/skills/gitnexus-exploring.md index 2dcf7b578..ccf684c28 100644 --- a/gitnexus/skills/gitnexus-exploring.md +++ b/gitnexus/skills/gitnexus-exploring.md @@ -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: ""}) → Find related execution flows -4. gitnexus_context({name: ""}) → Deep dive on specific symbol +3. query({query: ""}) → Find related execution flows +4. context({name: ""}) → 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 diff --git a/gitnexus/skills/gitnexus-impact-analysis.md b/gitnexus/skills/gitnexus-impact-analysis.md index 7206ca506..45eb7ce87 100644 --- a/gitnexus/skills/gitnexus-impact-analysis.md +++ b/gitnexus/skills/gitnexus-impact-analysis.md @@ -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) diff --git a/gitnexus/skills/gitnexus-pr-review.md b/gitnexus/skills/gitnexus-pr-review.md index 319c063f9..9f1d362e5 100644 --- a/gitnexus/skills/gitnexus-pr-review.md +++ b/gitnexus/skills/gitnexus-pr-review.md @@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what ``` 1. gh pr diff → 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: "", direction: "upstream"}) → Blast radius per change -4. gitnexus_context({name: ""}) → Understand callers/callees + impact({target: "", direction: "upstream"}) → Blast radius per change +4. context({name: ""}) → 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: diff --git a/gitnexus/skills/gitnexus-refactoring.md b/gitnexus/skills/gitnexus-refactoring.md index c749eb384..e13c04e14 100644 --- a/gitnexus/skills/gitnexus-refactoring.md +++ b/gitnexus/skills/gitnexus-refactoring.md @@ -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 ``` diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 6470c6430..641e7ee94 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -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 diff --git a/gitnexus/src/cli/skill-gen.ts b/gitnexus/src/cli/skill-gen.ts index e4e66e85b..f2fb42838 100644 --- a/gitnexus/src/cli/skill-gen.ts +++ b/gitnexus/src/cli/skill-gen.ts @@ -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(''); diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 24e68facb..46ecf4030 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -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 { const entries = await listRegisteredRepos({ validate: true }); - const freshIds = new Set(); - 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(); + const nextContext = new Map(); + const assigned = new Map(); // 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 { 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 { + /** + * 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 { + 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 }, ): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { 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 { 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 diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts index 88e7a99cc..707a5e047 100644 --- a/gitnexus/src/mcp/resources.ts +++ b/gitnexus/src/mcp/resources.ts @@ -292,7 +292,7 @@ async function getReposResource(backend: LocalBackend): Promise { 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.`, ); } diff --git a/gitnexus/test/unit/ai-context.test.ts b/gitnexus/test/unit/ai-context.test.ts index 6c3e96096..dc732f1fc 100644 --- a/gitnexus/test/unit/ai-context.test.ts +++ b/gitnexus/test/unit/ai-context.test.ts @@ -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 — Code Intelligence -- run \`gitnexus_detect_changes({scope: "compare", base_ref: "main"})\`. +- run \`detect_changes({scope: "compare", base_ref: "main"})\`. | Task | Read this skill file | |------|---------------------| diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index f645c1b19..422c67548 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -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 = {}; + 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', () => { diff --git a/gitnexus/test/unit/resources.test.ts b/gitnexus/test/unit/resources.test.ts index ccd8461f1..598698b8c 100644 --- a/gitnexus/test/unit/resources.test.ts +++ b/gitnexus/test/unit/resources.test.ts @@ -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_/); }); }); diff --git a/gitnexus/test/unit/skill-gen.test.ts b/gitnexus/test/unit/skill-gen.test.ts index 0da67061c..8e8800be6 100644 --- a/gitnexus/test/unit/skill-gen.test.ts +++ b/gitnexus/test/unit/skill-gen.test.ts @@ -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.