Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1f35d1a8 |
@@ -11,7 +11,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.7",
|
||||
"version": "1.3.3",
|
||||
"source": "./gitnexus-claude-plugin",
|
||||
"description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase."
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. context({name: "<suspect>"}) → See callers/callees/processes
|
||||
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
|
||||
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
|
||||
4. cypher({query: "MATCH path..."}) → Custom traces if needed
|
||||
4. gitnexus_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)
|
||||
- [ ] query for error text or related code
|
||||
- [ ] gitnexus_query for error text or related code
|
||||
- [ ] Identify the suspect function from returned processes
|
||||
- [ ] context to see callers and callees
|
||||
- [ ] gitnexus_context to see callers and callees
|
||||
- [ ] Trace execution flow via process resource if applicable
|
||||
- [ ] cypher for custom call chain traces if needed
|
||||
- [ ] gitnexus_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 | `query` for error text → `context` on throw sites |
|
||||
| Error message | `gitnexus_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
|
||||
|
||||
**query** — find code related to error:
|
||||
**gitnexus_query** — find code related to error:
|
||||
|
||||
```
|
||||
query({query: "payment validation error"})
|
||||
gitnexus_query({query: "payment validation error"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError, PaymentException
|
||||
```
|
||||
|
||||
**context** — full context for a suspect:
|
||||
**gitnexus_context** — full context for a suspect:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
→ Processes: CheckoutFlow (step 3/7)
|
||||
```
|
||||
|
||||
**cypher** — custom call chain traces:
|
||||
**gitnexus_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. query({query: "payment error handling"})
|
||||
1. gitnexus_query({query: "payment error handling"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError
|
||||
|
||||
2. context({name: "validatePayment"})
|
||||
2. gitnexus_context({name: "validatePayment"})
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
|
||||
@@ -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. query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
||||
```
|
||||
|
||||
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
|
||||
|
||||
```
|
||||
- [ ] READ gitnexus://repo/{name}/context
|
||||
- [ ] query for the concept you want to understand
|
||||
- [ ] gitnexus_query for the concept you want to understand
|
||||
- [ ] Review returned processes (execution flows)
|
||||
- [ ] context on key symbols for callers/callees
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**query** — find execution flows related to a concept:
|
||||
**gitnexus_query** — find execution flows related to a concept:
|
||||
|
||||
```
|
||||
query({query: "payment processing"})
|
||||
gitnexus_query({query: "payment processing"})
|
||||
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Symbols grouped by flow with file locations
|
||||
```
|
||||
|
||||
**context** — 360-degree view of a symbol:
|
||||
**gitnexus_context** — 360-degree view of a symbol:
|
||||
|
||||
```
|
||||
context({name: "validateUser"})
|
||||
gitnexus_context({name: "validateUser"})
|
||||
→ Incoming calls: loginHandler, apiMiddleware
|
||||
→ Outgoing calls: checkToken, getUserById
|
||||
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
|
||||
@@ -68,10 +68,10 @@ context({name: "validateUser"})
|
||||
|
||||
```
|
||||
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
|
||||
2. query({query: "payment processing"})
|
||||
2. gitnexus_query({query: "payment processing"})
|
||||
→ CheckoutFlow: processPayment → validateCard → chargeStripe
|
||||
→ RefundFlow: initiateRefund → calculateRefund → processRefund
|
||||
3. context({name: "processPayment"})
|
||||
3. gitnexus_context({name: "processPayment"})
|
||||
→ Incoming: checkoutHandler, webhookHandler
|
||||
→ Outgoing: validateCard, chargeStripe, saveTransaction
|
||||
4. Read src/payments/processor.ts for implementation details
|
||||
|
||||
@@ -38,38 +38,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
||||
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
||||
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
||||
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
||||
| `list_repos` | Discover indexed repos (paginated — `limit`/`offset`) |
|
||||
|
||||
### Paginating `list_repos`
|
||||
|
||||
`list_repos` is paginated so a large registry is not truncated by MCP/LLM token limits. It takes optional `limit` (default **50**, max **200**) and `offset`, and returns:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"repositories": [
|
||||
{ "name": "...", "path": "...", "indexedAt": "...", "lastCommit": "...", "stats": { } }
|
||||
],
|
||||
"pagination": {
|
||||
"total": 437,
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
"returned": 50,
|
||||
"hasMore": true,
|
||||
"nextOffset": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To enumerate **every** repository, keep calling with `offset` set to `pagination.nextOffset` until `hasMore` is `false`:
|
||||
|
||||
```text
|
||||
list_repos {} → repos 1–50, nextOffset 50, hasMore true
|
||||
list_repos { offset: 50 } → repos 51–100, nextOffset 100, hasMore true
|
||||
…
|
||||
list_repos { offset: 400 } → repos 401–437, hasMore false (done)
|
||||
```
|
||||
|
||||
Notes: `offset` ≥ `total` returns an empty page (with `total` still reported). Out-of-range or malformed `limit`/`offset` (non-integer, `limit` outside `[1, 200]`, `offset < 0`) are rejected with a clear error — `limit` above the max is rejected, not silently capped. The order is deterministic (lower-cased name, then path), so paging never skips or duplicates an entry while the registry is unchanged.
|
||||
| `list_repos` | Discover indexed repos |
|
||||
|
||||
## Resources Reference
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
3. detect_changes() → Map current git changes to affected flows
|
||||
3. gitnexus_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
|
||||
|
||||
```
|
||||
- [ ] impact({target, direction: "upstream"}) to find dependents
|
||||
- [ ] gitnexus_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
|
||||
- [ ] detect_changes() for pre-commit check
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**impact** — the primary tool for symbol blast radius:
|
||||
**gitnexus_impact** — the primary tool for symbol blast radius:
|
||||
|
||||
```
|
||||
impact({
|
||||
gitnexus_impact({
|
||||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
@@ -73,10 +73,10 @@ impact({
|
||||
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**detect_changes** — git-diff based impact analysis:
|
||||
**gitnexus_detect_changes** — git-diff based impact analysis:
|
||||
|
||||
```
|
||||
detect_changes({scope: "staged"})
|
||||
gitnexus_detect_changes({scope: "staged"})
|
||||
|
||||
→ Changed: 5 symbols in 3 files
|
||||
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
|
||||
@@ -86,7 +86,7 @@ detect_changes({scope: "staged"})
|
||||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
|
||||
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
1. gh pr diff <number> → Get the raw diff
|
||||
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
3. For each changed symbol:
|
||||
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. context({name: "<key symbol>"}) → Understand callers/callees
|
||||
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
|
||||
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
6. Summarize findings with risk assessment
|
||||
```
|
||||
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
|
||||
- [ ] detect_changes to map changes to affected execution flows
|
||||
- [ ] impact on each non-trivial changed symbol
|
||||
- [ ] gitnexus_detect_changes to map changes to affected execution flows
|
||||
- [ ] gitnexus_impact on each non-trivial changed symbol
|
||||
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
|
||||
- [ ] context on key changed symbols to understand full picture
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**detect_changes** — map PR diff to affected execution flows:
|
||||
**gitnexus_detect_changes** — map PR diff to affected execution flows:
|
||||
|
||||
```
|
||||
detect_changes({scope: "compare", base_ref: "main"})
|
||||
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
|
||||
→ Changed: 8 symbols in 4 files
|
||||
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**impact** — blast radius per changed symbol:
|
||||
**gitnexus_impact** — blast radius per changed symbol:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream"})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
|
||||
@@ -86,20 +86,20 @@ impact({target: "validatePayment", direction: "upstream"})
|
||||
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**impact with tests** — check test coverage:
|
||||
**gitnexus_impact with tests** — check test coverage:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
|
||||
→ Tests that cover this symbol:
|
||||
- validatePayment.test.ts [direct]
|
||||
- checkout.integration.test.ts [via processCheckout]
|
||||
```
|
||||
|
||||
**context** — understand a changed symbol's role:
|
||||
**gitnexus_context** — understand a changed symbol's role:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates
|
||||
@@ -112,20 +112,20 @@ context({name: "validatePayment"})
|
||||
1. gh pr diff 42 > /tmp/pr42.diff
|
||||
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
|
||||
|
||||
2. detect_changes({scope: "compare", base_ref: "main"})
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
→ Changed symbols: validatePayment, PaymentInput, formatAmount
|
||||
→ Affected processes: CheckoutFlow, RefundFlow
|
||||
→ Risk: MEDIUM
|
||||
|
||||
3. impact({target: "validatePayment", direction: "upstream"})
|
||||
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
→ d=1: processCheckout, webhookHandler (WILL BREAK)
|
||||
→ webhookHandler is NOT in the PR diff — potential breakage!
|
||||
|
||||
4. impact({target: "PaymentInput", direction: "upstream"})
|
||||
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
|
||||
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
|
||||
→ createPayment uses the old PaymentInput shape — breaking change!
|
||||
|
||||
5. context({name: "formatAmount"})
|
||||
5. gitnexus_context({name: "formatAmount"})
|
||||
→ Called by 12 functions — but change is backwards-compatible (added optional param)
|
||||
|
||||
6. Review summary:
|
||||
|
||||
@@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
||||
## Workflow
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
```
|
||||
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
|
||||
- [ ] gitnexus_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: rename({..., dry_run: false}) — apply edits
|
||||
- [ ] detect_changes() — verify only expected files changed
|
||||
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
|
||||
- [ ] gitnexus_detect_changes() — verify only expected files changed
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Extract Module
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] Define new module interface
|
||||
- [ ] Extract code, update imports
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Split Function/Service
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — understand all callees
|
||||
- [ ] gitnexus_context({name: target}) — understand all callees
|
||||
- [ ] Group callees by responsibility
|
||||
- [ ] impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] Create new functions/services
|
||||
- [ ] Update callers
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
**rename** — automated multi-file rename:
|
||||
**gitnexus_rename** — automated multi-file rename:
|
||||
|
||||
```
|
||||
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
gitnexus_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}]}]
|
||||
```
|
||||
|
||||
**impact** — map all dependents first:
|
||||
**gitnexus_impact** — map all dependents first:
|
||||
|
||||
```
|
||||
impact({target: "validateUser", direction: "upstream"})
|
||||
gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware, testUtils
|
||||
→ Affected Processes: LoginFlow, TokenRefresh
|
||||
```
|
||||
|
||||
**detect_changes** — verify your changes after refactoring:
|
||||
**gitnexus_detect_changes** — verify your changes after refactoring:
|
||||
|
||||
```
|
||||
detect_changes({scope: "all"})
|
||||
gitnexus_detect_changes({scope: "all"})
|
||||
→ Changed: 8 files, 12 symbols
|
||||
→ Affected processes: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**cypher** — custom reference queries:
|
||||
**gitnexus_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 rename for automated updates |
|
||||
| Many callers (>5) | Use gitnexus_rename for automated updates |
|
||||
| Cross-area refs | Use detect_changes after to verify scope |
|
||||
| String/dynamic refs | query to find them |
|
||||
| String/dynamic refs | gitnexus_query to find them |
|
||||
| External/public API | Version and deprecate properly |
|
||||
|
||||
## Example: Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
1. gitnexus_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. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
→ Applied 12 edits across 8 files
|
||||
|
||||
4. detect_changes({scope: "all"})
|
||||
4. gitnexus_detect_changes({scope: "all"})
|
||||
→ Affected: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM — run tests for these flows
|
||||
```
|
||||
|
||||
@@ -310,8 +310,8 @@ VS Code's Ports panel shows forwarded ports once their listener starts.
|
||||
|
||||
- **LadybugDB integration tests may fail in containers** (file-locking, `AGENTS.md` § Testing). Default to `npm run test:unit` inside the container; run integration tests on the host. Tracking issue: documented as a known limitation.
|
||||
- **Single-writer LadybugDB constraint** (`GUARDRAILS.md` § LadybugDB lock). Don't run `gitnexus analyze` on the host and inside the container against the same `.gitnexus/` directory simultaneously — the second writer will get `database busy`.
|
||||
- **Native grammar builds add ~30s to first install.** Tree-sitter Dart/Proto/Swift/Kotlin are all vendored uniformly: `node-gyp-build` picks a committed GitNexus-built prebuilt `.node` at install time (no compile), and only falls back to compiling from the vendored source during `postinstall` if no prebuild matches the host (then a toolchain is needed). Set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` (in your shell or `remoteEnv`, then rebuild) to skip all four; each loses parsing for the affected language(s), and the install still succeeds.
|
||||
- **`tree-sitter-kotlin`/`tree-sitter-swift` warnings on install** only appear when no prebuild matches the platform-arch (per `AGENTS.md`); they are non-fatal — parsing for that language is simply unavailable.
|
||||
- **Native grammar builds add ~30s to first install.** Tree-sitter Dart/Proto/Swift grammars build during `gitnexus`'s `postinstall`. To skip them (loses parsing for those three languages), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` in your shell or add it to `remoteEnv` and rebuild.
|
||||
- **`tree-sitter-kotlin` warnings on install** are expected (per `AGENTS.md`). Ignore them.
|
||||
- **`.mcp.json` works inside the container**: `npx -y gitnexus@latest mcp` resolves cleanly because npm registry is reachable and the workspace bind mount exposes the same `.mcp.json` the host sees.
|
||||
- **Husky pre-commit fires inside the container** without extra setup. The root `npm install` (run automatically in `postCreateCommand`) installs the hook via `package.json` `prepare`.
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Code owners
|
||||
|
||||
* @Arvuno
|
||||
* @magyargergo
|
||||
@@ -1,266 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Vendored tree-sitter grammar update monitor.
|
||||
*
|
||||
* Checks each vendored grammar against its upstream source-of-origin and, for an
|
||||
* available AND ABI-compatible update, re-vendors the grammar source in place so
|
||||
* a PR can be opened. The version bump in vendor/<name>/package.json then triggers
|
||||
* .github/workflows/build-tree-sitter-prebuilds.yml, which cross-builds + ABI-
|
||||
* validates the prebuilds — so even an imperfect re-vendor can never silently
|
||||
* ship: its PR's CI goes red.
|
||||
*
|
||||
* ABI awareness is load-bearing. Every grammar is pinned to tree-sitter@0.21.1
|
||||
* (LANGUAGE_VERSION 13–14, the #1922 gate). Most upstream grammar releases target
|
||||
* a newer tree-sitter, so a blind "bump to latest" would pull an ABI-incompatible
|
||||
* parser and open doomed PRs. This monitor fetches the candidate source, reads its
|
||||
* parser.c `#define LANGUAGE_VERSION`, and only re-vendors when it is 13 or 14;
|
||||
* incompatible updates are reported (and surfaced as a workflow notice), not
|
||||
* applied.
|
||||
*
|
||||
* Usage:
|
||||
* node update-vendored-grammars.mjs # detect only → JSON report on stdout
|
||||
* node update-vendored-grammars.mjs --apply X # re-vendor grammar X in place
|
||||
*
|
||||
* tree-sitter-c is MONITORED but report-only (`hold`): it is ABI-pinned at 0.21.4
|
||||
* (#1242/#858) and must not auto-bump without a tree-sitter runtime upgrade, so an
|
||||
* available c update is detected + reported but never auto-applied — even if it is
|
||||
* ABI-13/14. A maintainer re-vendors it deliberately.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
||||
const VENDOR = path.join(REPO_ROOT, 'gitnexus', 'vendor');
|
||||
|
||||
const COMPATIBLE_ABI = new Set([13, 14]); // tree-sitter@0.21.1 LANGUAGE_VERSION range
|
||||
|
||||
// Source-of-origin per grammar. npm grammars resolve `latest` via the registry;
|
||||
// github grammars (no usable npm release) track the default branch HEAD. A `hold`
|
||||
// reason makes a grammar report-only: updates are detected + surfaced but never
|
||||
// auto-applied (c is ABI-pinned and must not move without a runtime upgrade).
|
||||
const GRAMMARS = {
|
||||
c: {
|
||||
name: 'tree-sitter-c',
|
||||
npm: 'tree-sitter-c',
|
||||
hold: 'ABI-pinned at 0.21.4 (#1242/#858) — needs a tree-sitter runtime upgrade before bumping',
|
||||
},
|
||||
swift: { name: 'tree-sitter-swift', npm: 'tree-sitter-swift' },
|
||||
kotlin: { name: 'tree-sitter-kotlin', npm: 'tree-sitter-kotlin' },
|
||||
dart: { name: 'tree-sitter-dart', github: 'UserNobody14/tree-sitter-dart' },
|
||||
proto: { name: 'tree-sitter-proto', github: 'coder3101/tree-sitter-proto' },
|
||||
};
|
||||
|
||||
const sh = (cmd, args, opts = {}) =>
|
||||
execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts }).trim();
|
||||
|
||||
const clean = (v) =>
|
||||
String(v || '')
|
||||
.replace(/^[v^~]/, '')
|
||||
.trim();
|
||||
|
||||
function vendoredVersion(g) {
|
||||
const p = path.join(VENDOR, g.name, 'package.json');
|
||||
return clean(JSON.parse(fs.readFileSync(p, 'utf8')).version);
|
||||
}
|
||||
|
||||
/** Resolve the upstream candidate: { version, ref, kind }. */
|
||||
function resolveUpstream(g) {
|
||||
if (g.npm) {
|
||||
const version = clean(sh('npm', ['view', g.npm, 'version']));
|
||||
return { version, ref: version, kind: 'npm' };
|
||||
}
|
||||
// github: no reliable release tags here, so track the default branch HEAD sha.
|
||||
const meta = JSON.parse(sh('gh', ['api', `repos/${g.github}`]));
|
||||
const branch = meta.default_branch;
|
||||
const sha = JSON.parse(sh('gh', ['api', `repos/${g.github}/commits/${branch}`])).sha;
|
||||
// Version key: "<upstreamPkgVersion>-g<sha7>" — safeRef-compatible (no `+`,
|
||||
// which the build workflow's ref validator rejects) and changes on every commit.
|
||||
let base = '0.0.0';
|
||||
try {
|
||||
const pkg = JSON.parse(
|
||||
Buffer.from(
|
||||
JSON.parse(sh('gh', ['api', `repos/${g.github}/contents/package.json?ref=${sha}`])).content,
|
||||
'base64',
|
||||
).toString('utf8'),
|
||||
);
|
||||
if (pkg.version) base = clean(pkg.version);
|
||||
} catch {
|
||||
/* no upstream package.json — base stays 0.0.0 */
|
||||
}
|
||||
return { version: `${base}-g${sha.slice(0, 7)}`, ref: sha, kind: 'github' };
|
||||
}
|
||||
|
||||
/** Fetch the candidate source into a temp dir; return the package root. */
|
||||
function fetchSource(g, ref) {
|
||||
const work = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `revendor-${Object.keys(GRAMMARS).find((k) => GRAMMARS[k] === g)}-`),
|
||||
);
|
||||
if (g.npm) {
|
||||
sh('npm', ['pack', `${g.npm}@${ref}`, '--silent'], { cwd: work });
|
||||
const tgz = fs.readdirSync(work).find((f) => f.endsWith('.tgz'));
|
||||
sh('tar', ['xzf', tgz], { cwd: work });
|
||||
return path.join(work, 'package');
|
||||
}
|
||||
// github tarball at the resolved sha. Download + extract WITHOUT a shell
|
||||
// (no `bash -c`/redirect): `gh api` writes the binary tarball to stdout, which
|
||||
// we capture as a Buffer and write to a fixed path, then extract with execFile.
|
||||
// Avoids the shell-command-injection surface CodeQL flags when an API-derived
|
||||
// ref is interpolated into a `bash -c` string.
|
||||
const tgz = path.join(work, 'src.tgz');
|
||||
fs.writeFileSync(
|
||||
tgz,
|
||||
execFileSync('gh', ['api', `repos/${g.github}/tarball/${ref}`], {
|
||||
maxBuffer: 512 * 1024 * 1024,
|
||||
}),
|
||||
);
|
||||
sh('tar', ['xzf', tgz], { cwd: work });
|
||||
const dir = fs.readdirSync(work).find((f) => fs.statSync(path.join(work, f)).isDirectory());
|
||||
return path.join(work, dir);
|
||||
}
|
||||
|
||||
/** Read parser.c's LANGUAGE_VERSION (ABI). Prefer the ABI-14 default parser.c. */
|
||||
function readAbi(srcRoot) {
|
||||
const candidates = ['src/parser.c', 'parser.c'];
|
||||
for (const rel of candidates) {
|
||||
const p = path.join(srcRoot, rel);
|
||||
if (!fs.existsSync(p)) continue;
|
||||
// Read only the head — the #define is near the top.
|
||||
const head = fs.readFileSync(p, 'utf8').slice(0, 4000);
|
||||
const m = head.match(/#define\s+LANGUAGE_VERSION\s+(\d+)/);
|
||||
if (m) return Number(m[1]);
|
||||
}
|
||||
return null; // unknown (e.g. parser.c only generated at build time)
|
||||
}
|
||||
|
||||
function detect() {
|
||||
const report = [];
|
||||
for (const [key, g] of Object.entries(GRAMMARS)) {
|
||||
const have = vendoredVersion(g);
|
||||
let up;
|
||||
try {
|
||||
up = resolveUpstream(g);
|
||||
} catch (err) {
|
||||
report.push({ grammar: key, error: String(err.message || err) });
|
||||
continue;
|
||||
}
|
||||
const newer = up.kind === 'npm' ? up.version !== have : !have || up.ref.slice(0, 7) !== have;
|
||||
let abi = null;
|
||||
if (newer) {
|
||||
try {
|
||||
abi = readAbi(fetchSource(g, up.ref));
|
||||
} catch {
|
||||
/* fetch/abi best-effort; null = unknown */
|
||||
}
|
||||
}
|
||||
report.push({
|
||||
grammar: key,
|
||||
vendored: have,
|
||||
upstream: up.version,
|
||||
ref: up.ref,
|
||||
kind: up.kind,
|
||||
update: newer,
|
||||
abi,
|
||||
abiCompatible: abi == null ? null : COMPATIBLE_ABI.has(abi),
|
||||
hold: g.hold || null,
|
||||
// Auto-appliable only when there's an update, the ABI is known-compatible,
|
||||
// AND the grammar is not on a policy hold (c).
|
||||
applicable: newer && abi != null && COMPATIBLE_ABI.has(abi) && !g.hold,
|
||||
});
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
const copyFile = (srcRoot, dest, rel) => {
|
||||
const from = path.join(srcRoot, rel);
|
||||
if (!fs.existsSync(from)) return false;
|
||||
const to = path.join(dest, rel);
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });
|
||||
fs.copyFileSync(from, to);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-vendor one grammar in place from its ABI-compatible upstream candidate.
|
||||
* Copies ONLY the generated source-build + runtime files; deliberately KEEPS the
|
||||
* GitNexus-hardened binding.gyp (Windows cflags, target_name), README (vendor
|
||||
* notice), LICENSE, and prebuilds/ (the build workflow refreshes those). Bumps the
|
||||
* stripped vendor package.json version + provenance — never re-introduces
|
||||
* scripts/dependencies (#836/#1728). Returns the new version.
|
||||
*/
|
||||
function apply(key) {
|
||||
const g = GRAMMARS[key];
|
||||
if (!g) {
|
||||
console.error(`unknown grammar '${key}'`);
|
||||
process.exit(2);
|
||||
}
|
||||
if (g.hold) {
|
||||
console.error(
|
||||
`${key}: report-only (${g.hold}); not auto-applied. Re-vendor manually if intended.`,
|
||||
);
|
||||
process.exit(3);
|
||||
}
|
||||
const have = vendoredVersion(g);
|
||||
const up = resolveUpstream(g);
|
||||
const newer = up.kind === 'npm' ? up.version !== have : !have || up.version !== have;
|
||||
if (!newer) {
|
||||
console.error(`${key}: already current (${have}); nothing to apply.`);
|
||||
process.exit(0);
|
||||
}
|
||||
const srcRoot = fetchSource(g, up.ref);
|
||||
const abi = readAbi(srcRoot);
|
||||
if (abi == null || !COMPATIBLE_ABI.has(abi)) {
|
||||
console.error(
|
||||
`${key}: candidate ${up.version} is ABI ${abi ?? 'unknown'} — not tree-sitter@0.21.1 ` +
|
||||
`compatible (need 13/14); refusing to re-vendor. Handle manually.`,
|
||||
);
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
const dest = path.join(VENDOR, g.name);
|
||||
// The source-build inputs + runtime entrypoints that change between versions.
|
||||
// binding.gyp / README / LICENSE / prebuilds are intentionally NOT touched.
|
||||
for (const rel of [
|
||||
'src/parser.c',
|
||||
'src/scanner.c',
|
||||
'src/node-types.json',
|
||||
'src/tree_sitter/alloc.h',
|
||||
'src/tree_sitter/array.h',
|
||||
'src/tree_sitter/parser.h',
|
||||
'bindings/node/binding.cc',
|
||||
'bindings/node/index.js',
|
||||
'bindings/node/index.d.ts',
|
||||
]) {
|
||||
copyFile(srcRoot, dest, rel);
|
||||
}
|
||||
|
||||
const pkgPath = path.join(dest, 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
pkg.version = up.version;
|
||||
pkg._vendoredBy =
|
||||
`gitnexus - re-vendored from ${g.npm ? `npm ${g.npm}@${up.version}` : `${g.github}@${up.ref}`} ` +
|
||||
`by grammar-update-monitor on ABI ${abi}. Source-build inputs (parser.c/scanner.c/src/) refreshed; ` +
|
||||
`the GitNexus-hardened binding.gyp + vendor README + prebuilds are preserved (prebuilds are ` +
|
||||
`rebuilt by build-tree-sitter-prebuilds.yml on this version change). No scripts/dependencies here ` +
|
||||
`(#836/#1728).`;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
|
||||
console.log(`${key}: re-vendored ${g.name} → ${up.version} (ABI ${abi}).`);
|
||||
return up.version;
|
||||
}
|
||||
|
||||
// Run the CLI only when invoked directly (not when imported by a test) — detect()
|
||||
// makes live network calls, so importing must be side-effect-free.
|
||||
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
if (isMain) {
|
||||
if (process.argv[2] === '--apply') {
|
||||
apply(process.argv[3]);
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify(detect(), null, 2) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
export { detect, apply, resolveUpstream, readAbi, vendoredVersion, GRAMMARS, COMPATIBLE_ABI };
|
||||
@@ -1,529 +0,0 @@
|
||||
name: Build tree-sitter prebuilds
|
||||
|
||||
# Cross-builds the native tree-sitter prebuilds GitNexus vendors itself, so that
|
||||
# grammars whose upstream packages ship SOURCE ONLY (no usable prebuilds/) never
|
||||
# require a C/C++ toolchain at a user's install. This is the "no operational
|
||||
# risk for any tree-sitter grammar" pipeline.
|
||||
#
|
||||
# Grammars covered here (the at-risk set — everything else already ships 6
|
||||
# upstream prebuilds AND stays dependency-review-tracked, so it is left alone).
|
||||
# All five are vendored under gitnexus/vendor/; `kind` (below) only picks where
|
||||
# the build job fetches the C source to compile:
|
||||
# - tree-sitter-c (vendored prebuild-only; built from the published npm
|
||||
# package — closes upstream's 4/6 ARM gap #2116 for a
|
||||
# REQUIRED grammar)
|
||||
# - tree-sitter-dart (vendored source; built from gitnexus/vendor/)
|
||||
# - tree-sitter-proto (vendored source; built from gitnexus/vendor/)
|
||||
# - tree-sitter-kotlin (vendored source; built from the published npm package —
|
||||
# upstream ships source only)
|
||||
# - tree-sitter-swift (vendored source; built from gitnexus/vendor/ — its
|
||||
# prebuilds were originally upstream-shipped, now
|
||||
# GitNexus-cross-built like the rest for uniformity)
|
||||
#
|
||||
# Output: gitnexus/vendor/<grammar>/prebuilds/<platform-arch>/<grammar>.node for
|
||||
# all 6 targets ({linux,darwin,win32}-{x64,arm64}). tree-sitter grammars are
|
||||
# N-API, so one ABI-stable .node per platform-arch works across all Node majors.
|
||||
#
|
||||
# COST DISCIPLINE — this is a HEAVY native matrix (up to 3 grammars x 6 runners,
|
||||
# incl. macOS + arm64). It is DELIBERATELY NOT wired into normal PR/push CI. It
|
||||
# runs only:
|
||||
# 1. on manual dispatch (workflow_dispatch); or
|
||||
# 2. when a covered grammar's recorded version actually CHANGES — the `guard`
|
||||
# job is the real gate (it diffs the recorded version vs the PR base); the
|
||||
# `paths:` filter below only makes ordinary code PRs cost ZERO matrix time.
|
||||
# Net effect: an ordinary code PR triggers nothing; bumping one grammar costs
|
||||
# exactly one matrix run for that grammar, which opens a PR committing its rebuilt
|
||||
# binaries.
|
||||
#
|
||||
# Concurrency convention: see CONTRIBUTING.md -> "GitHub Actions — Concurrency Convention".
|
||||
#
|
||||
# NOTE: every action below is pinned to a release commit SHA (with the matching
|
||||
# `# vX.Y.Z` tag comment verified against the GitHub API). If a future bump adds
|
||||
# a new action, pin its real release SHA and allowlist it in .github/zizmor.yml /
|
||||
# Scorecard before merge.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
grammars:
|
||||
description: 'Comma-separated grammar shortnames to build (c,dart,proto,kotlin,swift), or "all".'
|
||||
required: false
|
||||
type: string
|
||||
default: 'all'
|
||||
ref:
|
||||
description: 'Upstream version/tag/sha override (only honored when exactly one grammar is selected).'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
force:
|
||||
description: 'Build even if the recorded version is unchanged (re-cut a broken prebuild).'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
open_pr:
|
||||
description: 'Open a PR with the rebuilt prebuilds (false = artifacts only).'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
# Vendored grammars: their version lives in the vendor snapshot package.json.
|
||||
- 'gitnexus/vendor/tree-sitter-c/package.json'
|
||||
- 'gitnexus/vendor/tree-sitter-dart/package.json'
|
||||
- 'gitnexus/vendor/tree-sitter-proto/package.json'
|
||||
- 'gitnexus/vendor/tree-sitter-kotlin/package.json'
|
||||
- 'gitnexus/vendor/tree-sitter-swift/package.json'
|
||||
# Transition window: kotlin's pin still lives here until it is vendored.
|
||||
- 'gitnexus/package.json'
|
||||
# Self-test: re-run the guard (normally a no-op) when the recipe changes.
|
||||
- '.github/workflows/build-tree-sitter-prebuilds.yml'
|
||||
|
||||
# Least privilege by default; only `aggregate` opts up.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# One slot per ref. Collapse PR re-pushes, but never cancel a manual re-cut.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
# ── Gate: decide which grammars (if any) need a native rebuild, and emit the
|
||||
# {grammar x platform-arch} matrix the build job consumes. ───────────────
|
||||
guard:
|
||||
name: Decide what to build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
any: ${{ steps.decide.outputs.any }}
|
||||
matrix: ${{ steps.decide.outputs.matrix }}
|
||||
release_app: ${{ steps.relapp.outputs.configured }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0 # need base history to diff recorded versions
|
||||
persist-credentials: false
|
||||
|
||||
- name: Decide
|
||||
id: decide
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
# Untrusted dispatch inputs — read via env only, validated in JS.
|
||||
INPUT_GRAMMARS: ${{ inputs.grammars }}
|
||||
INPUT_REF: ${{ inputs.ref }}
|
||||
FORCE: ${{ github.event_name == 'workflow_dispatch' && inputs.force || 'false' }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module - <<'NODE'
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
|
||||
// Registry of the at-risk grammars this workflow owns. `kind` drives
|
||||
// how the build job resolves source: 'npm' pulls the published package;
|
||||
// 'vendored' builds from gitnexus/vendor/<name> (which carries the C
|
||||
// source + binding.gyp). Extend this list to cover a new grammar.
|
||||
const REGISTRY = {
|
||||
// c is vendored prebuild-only but BUILT from the published npm
|
||||
// package (kind 'npm'), held at 0.21.4 — it closes upstream's 4/6
|
||||
// ARM gap (#2116) for a REQUIRED grammar that otherwise hard-fails
|
||||
// install on toolchain-less ARM.
|
||||
c: { name: 'tree-sitter-c', kind: 'npm' },
|
||||
dart: { name: 'tree-sitter-dart', kind: 'vendored' },
|
||||
proto: { name: 'tree-sitter-proto', kind: 'vendored' },
|
||||
kotlin: { name: 'tree-sitter-kotlin', kind: 'npm' },
|
||||
// swift is vendored WITH its source (parser.c/scanner.c/binding.gyp),
|
||||
// so it builds from gitnexus/vendor/ like dart/proto. Its prebuilds
|
||||
// were originally upstream-shipped; rebuilding them here unifies it.
|
||||
swift: { name: 'tree-sitter-swift', kind: 'vendored' },
|
||||
};
|
||||
const PLATFORMS = [
|
||||
{ platform_arch: 'linux-x64', os: 'ubuntu-24.04' },
|
||||
{ platform_arch: 'linux-arm64', os: 'ubuntu-24.04-arm' },
|
||||
{ platform_arch: 'darwin-arm64', os: 'macos-15' },
|
||||
{ platform_arch: 'darwin-x64', os: 'macos-15-intel' }, // macos-13 retired Dec-2025; Intel EOL ~Aug-2027
|
||||
{ platform_arch: 'win32-x64', os: 'windows-2022' },
|
||||
{ platform_arch: 'win32-arm64', os: 'windows-11-arm' },
|
||||
];
|
||||
|
||||
const clean = (v) => (v || '').replace(/^[\^~]/, '').trim();
|
||||
const json = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
|
||||
|
||||
// Durable version key for a grammar at a checkout root. Prefer the
|
||||
// vendor snapshot (the post-vendor source of truth); fall back to the
|
||||
// optionalDependencies pin during the transition window. (A guard keyed
|
||||
// on the node_modules lock entry would self-disable once a grammar is
|
||||
// vendored, because that entry is deleted.)
|
||||
function recordedVersion(root, name) {
|
||||
const v = json(`${root}/gitnexus/vendor/${name}/package.json`);
|
||||
if (v && v.version) return clean(v.version);
|
||||
const pkg = json(`${root}/gitnexus/package.json`);
|
||||
const od = pkg && (pkg.optionalDependencies || {});
|
||||
const d = pkg && (pkg.dependencies || {});
|
||||
return clean((od && od[name]) || (d && d[name]) || '');
|
||||
}
|
||||
|
||||
const event = process.env.EVENT;
|
||||
const force = process.env.FORCE === 'true';
|
||||
|
||||
// Select which grammar shortnames are in play.
|
||||
let selected;
|
||||
if (event === 'workflow_dispatch') {
|
||||
const raw = (process.env.INPUT_GRAMMARS || 'all').trim();
|
||||
selected = raw === 'all' ? Object.keys(REGISTRY)
|
||||
: raw.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
for (const s of selected) if (!REGISTRY[s]) throw new Error(`unknown grammar '${s}'`);
|
||||
} else {
|
||||
selected = Object.keys(REGISTRY);
|
||||
}
|
||||
|
||||
// Resolve the base-ref recorded versions (pull_request only) so we can
|
||||
// diff. On dispatch, base is irrelevant (manual intent / force wins).
|
||||
const baseRoot = `${process.env.RUNNER_TEMP}/base`;
|
||||
if (event === 'pull_request') {
|
||||
const baseSha = process.env.BASE_SHA;
|
||||
for (const s of selected) {
|
||||
const name = REGISTRY[s].name;
|
||||
for (const rel of [`gitnexus/vendor/${name}/package.json`, `gitnexus/package.json`]) {
|
||||
const dst = `${baseRoot}/${rel}`;
|
||||
fs.mkdirSync(dst.slice(0, dst.lastIndexOf('/')), { recursive: true });
|
||||
try {
|
||||
const buf = execSync(`git show ${baseSha}:${rel}`, { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
fs.writeFileSync(dst, buf);
|
||||
} catch { /* file absent at base — fine */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The single-ref override is only meaningful for a one-grammar dispatch.
|
||||
const refOverride = clean(process.env.INPUT_REF);
|
||||
if (refOverride && !(event === 'workflow_dispatch' && selected.length === 1)) {
|
||||
throw new Error('ref override requires exactly one grammar selected');
|
||||
}
|
||||
const safeRef = (r) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(r);
|
||||
|
||||
const include = [];
|
||||
const built = [];
|
||||
for (const short of selected) {
|
||||
const { name, kind } = REGISTRY[short];
|
||||
const head = recordedVersion('.', name);
|
||||
const ref = refOverride || head;
|
||||
if (!ref) { console.log(`skip ${short}: no recorded version`); continue; }
|
||||
if (!safeRef(ref)) throw new Error(`unsafe ref for ${short}: '${ref}'`);
|
||||
|
||||
let build = false;
|
||||
if (event === 'workflow_dispatch') {
|
||||
build = true; // manual intent (force toggles only the unchanged-guard, which is bypassed here)
|
||||
} else {
|
||||
const base = recordedVersion(baseRoot, name);
|
||||
build = !!head && head !== base;
|
||||
console.log(`${short}: head='${head || '<absent>'}' base='${base || '<absent>'}' -> ${build ? 'BUILD' : 'skip'}`);
|
||||
}
|
||||
if (force) build = true;
|
||||
if (!build) continue;
|
||||
built.push(short);
|
||||
for (const p of PLATFORMS) include.push({ grammar: short, name, kind, ref, ...p });
|
||||
}
|
||||
|
||||
const out = process.env.GITHUB_OUTPUT;
|
||||
appendFileSync(out, `any=${include.length > 0}\n`);
|
||||
appendFileSync(out, `matrix=${JSON.stringify({ include })}\n`);
|
||||
if (include.length === 0) {
|
||||
console.log('::notice::No covered grammar version changed — skipping native matrix.');
|
||||
} else {
|
||||
console.log(`Building: ${built.join(', ')} (${include.length} jobs)`);
|
||||
}
|
||||
NODE
|
||||
|
||||
# The aggregate job opens a PR via a GitHub App token; without the App
|
||||
# secrets it would hard-fail AFTER a full native build. Surface their
|
||||
# presence as a guard output so aggregate skips cleanly (the build job's
|
||||
# artifacts still upload). secrets aren't available in a job-level `if:`,
|
||||
# so we compute the boolean here (a step CAN read secrets) and gate on it.
|
||||
- name: Check release App secret
|
||||
id: relapp
|
||||
env:
|
||||
HAS_APP: ${{ secrets.RELEASE_APP_ID != '' && secrets.RELEASE_APP_PRIVATE_KEY != '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "configured=$HAS_APP" >> "$GITHUB_OUTPUT"
|
||||
if [ "$HAS_APP" != "true" ]; then
|
||||
echo "::notice::Release GitHub App secrets (RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY) are not configured — prebuilds will build and upload as artifacts, but the auto-PR is skipped. Provision the App, or run with open_pr=false to suppress this notice."
|
||||
fi
|
||||
|
||||
# ── Build one native prebuild per (grammar, platform-arch). No cross-compile. ─
|
||||
build:
|
||||
name: ${{ matrix.grammar }} ${{ matrix.platform_arch }}
|
||||
needs: guard
|
||||
if: needs.guard.outputs.any == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.guard.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
# 45 (not 30) for headroom: the kotlin parser.c is ~23 MB and swift's ~18 MB,
|
||||
# and compiling them under emulation on the arm runners is slow.
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false # this job uploads artifacts (artipacked)
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Ensure Python (arm64 Windows only)
|
||||
if: matrix.platform_arch == 'win32-arm64'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Build prebuild
|
||||
id: build
|
||||
shell: bash
|
||||
env:
|
||||
GRAMMAR: ${{ matrix.grammar }}
|
||||
NAME: ${{ matrix.name }}
|
||||
KIND: ${{ matrix.kind }}
|
||||
REF: ${{ matrix.ref }}
|
||||
PLATFORM_ARCH: ${{ matrix.platform_arch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
work="$RUNNER_TEMP/ts-build"
|
||||
rm -rf "$work"; mkdir -p "$work"; cd "$work"
|
||||
npm init -y >/dev/null
|
||||
|
||||
# node-addon-api must match what the grammar's binding.cc expects.
|
||||
# GitNexus hoists ^8 for the vendored grammars; npm grammars declare
|
||||
# their own (do NOT pin it for npm grammars — let the dep resolve it).
|
||||
if [ "$KIND" = "vendored" ]; then
|
||||
# Build from the vendored C source (carries parser.c + binding.gyp).
|
||||
srcdir="$work/$NAME"
|
||||
cp -R "$GITHUB_WORKSPACE/gitnexus/vendor/$NAME" "$srcdir"
|
||||
rm -rf "$srcdir/prebuilds" "$srcdir/build" "$srcdir/node_modules"
|
||||
npm install --no-audit --no-fund --ignore-scripts \
|
||||
prebuildify@^6 node-gyp@^11 node-addon-api@^8
|
||||
pkgdir="$srcdir"
|
||||
export npm_config_node_gyp="$work/node_modules/node-gyp/bin/node-gyp.js"
|
||||
else
|
||||
# Pull the published source-only package.
|
||||
npm install --no-audit --no-fund --ignore-scripts \
|
||||
"$NAME@${REF}" prebuildify@^6 node-gyp@^11
|
||||
pkgdir="$work/node_modules/$NAME"
|
||||
fi
|
||||
|
||||
test -f "$pkgdir/binding.gyp" || { echo "::error::no binding.gyp for $NAME@$REF"; exit 1; }
|
||||
|
||||
# Drop any prebuilds the package shipped in its own tarball before we
|
||||
# build. The tree-sitter-org npm grammars (e.g. tree-sitter-c) bundle
|
||||
# prebuilds/ for all 6 tuples; left in place, the `find ... -print -quit`
|
||||
# below would pick a non-host tuple (e.g. win32-x64 on a linux runner)
|
||||
# and the assertion would wrongly fail. prebuildify rebuilds THIS host's
|
||||
# tuple from the source the tarball also ships. (Vendored grammars are
|
||||
# already cleaned above; this also covers the npm branch.)
|
||||
rm -rf "$pkgdir/prebuilds"
|
||||
|
||||
# N-API, stripped, single ABI-stable binary for THIS host's arch. No
|
||||
# `-t <node-version>`: an N-API prebuild is Node-version-agnostic, and
|
||||
# prebuildify parses a bare `-t 22` as the NUMBER 22 and crashes
|
||||
# (`v.indexOf is not a function`). prebuildify emits
|
||||
# prebuilds/<platform>-<arch>/<something>.node.
|
||||
( cd "$pkgdir" && npx --no-install prebuildify --napi --strip )
|
||||
|
||||
out=$(find "$pkgdir/prebuilds" -name '*.node' -print -quit)
|
||||
test -n "$out" || { echo "::error::prebuildify produced no .node"; exit 1; }
|
||||
produced=$(basename "$(dirname "$out")")
|
||||
[ "$produced" = "$PLATFORM_ARCH" ] || { echo "::error::built $produced, expected $PLATFORM_ARCH"; exit 1; }
|
||||
|
||||
stage="$RUNNER_TEMP/stage/$GRAMMAR/$PLATFORM_ARCH"; mkdir -p "$stage"
|
||||
cp "$out" "$stage/$NAME.node"
|
||||
echo "stage=$stage" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate the .node loads and parses on this arch
|
||||
shell: bash
|
||||
env:
|
||||
GRAMMAR: ${{ matrix.grammar }}
|
||||
NAME: ${{ matrix.name }}
|
||||
PLATFORM_ARCH: ${{ matrix.platform_arch }}
|
||||
EXPECT_ARCH: ${{ contains(matrix.platform_arch, 'arm64') && 'arm64' || 'x64' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
probe="$RUNNER_TEMP/probe"; rm -rf "$probe"
|
||||
mkdir -p "$probe/prebuilds/$PLATFORM_ARCH"
|
||||
cp "$RUNNER_TEMP/stage/$GRAMMAR/$PLATFORM_ARCH/$NAME.node" \
|
||||
"$probe/prebuilds/$PLATFORM_ARCH/$NAME.node"
|
||||
cd "$probe"
|
||||
# Pin tree-sitter to the repo's exact runtime peer so an ABI mismatch
|
||||
# fails HERE, not in a user's install (mirrors the #1922 ABI gate).
|
||||
# NOT --ignore-scripts: tree-sitter@0.21.1's tarball ships prebuilds for
|
||||
# the common tuples but NOT linux-arm64 / win32-arm64, so on the arm64
|
||||
# runners node-gyp-build must source-build the runtime — give it node-gyp
|
||||
# + node-addon-api to do so. Where tree-sitter ships a prebuild (x64,
|
||||
# darwin-arm64) node-gyp-build uses it and nothing compiles. The grammar
|
||||
# .node we built is still loaded as a prebuild; only the runtime peer may
|
||||
# compile. The grammar-vs-runtime ABI check still fires at setLanguage.
|
||||
npm install --no-audit --no-fund \
|
||||
node-gyp-build@^4 node-gyp@^11 node-addon-api@^8 tree-sitter@0.21.1
|
||||
# The node script is single-quoted on purpose — its ${...} are JS
|
||||
# template literals read from the environment, not shell expansions.
|
||||
# shellcheck disable=SC2016
|
||||
GRAMMAR="$GRAMMAR" EXPECT_ARCH="$EXPECT_ARCH" node -e '
|
||||
const expect = process.env.EXPECT_ARCH;
|
||||
// Catch an emulated x64 Node silently mis-passing on an arm64 runner.
|
||||
if (process.arch !== expect) throw new Error(`runner arch ${process.arch} != ${expect}`);
|
||||
const snippets = {
|
||||
c: "int main(void) { return 0; }",
|
||||
dart: "void main() { print(\"hi\"); }",
|
||||
proto: "syntax = \"proto3\";\nmessage M { int32 id = 1; }",
|
||||
kotlin: "fun main() { println(\"hi\") }",
|
||||
swift: "func greet() { print(\"hi\") }",
|
||||
};
|
||||
const lang = require("node-gyp-build")(process.cwd());
|
||||
const Parser = require("tree-sitter");
|
||||
const p = new Parser(); p.setLanguage(lang);
|
||||
const tree = p.parse(snippets[process.env.GRAMMAR]);
|
||||
if (!tree || !tree.rootNode || tree.rootNode.hasError) {
|
||||
throw new Error("parse failed/error: " + (tree && tree.rootNode && tree.rootNode.type));
|
||||
}
|
||||
console.log("OK", process.env.GRAMMAR, process.platform + "-" + process.arch, tree.rootNode.type);
|
||||
'
|
||||
|
||||
- name: Upload prebuild artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ts-prebuild-${{ matrix.grammar }}-${{ matrix.platform_arch }}
|
||||
path: ${{ steps.build.outputs.stage }}/${{ matrix.name }}.node
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
# ── Aggregate every grammar's six prebuilds, assert completeness, open a PR. ─
|
||||
aggregate:
|
||||
name: Vendor prebuilds + open PR
|
||||
needs: [guard, build]
|
||||
# Open the prebuild PR on a non-fork pull_request that bumped a grammar
|
||||
# version (the documented version-change -> prebuild-PR flow), or on a manual
|
||||
# dispatch with open_pr=true. Event-gating is explicit so we never rely on
|
||||
# GHA coercing a null `inputs.open_pr` on pull_request events (Codex F4):
|
||||
# `inputs.open_pr` is null off-dispatch, and `null != false` is direction-
|
||||
# ambiguous, so `open_pr` is only consulted on workflow_dispatch.
|
||||
if: >-
|
||||
needs.guard.outputs.any == 'true' &&
|
||||
needs.guard.outputs.release_app == 'true' &&
|
||||
github.event.pull_request.head.repo.fork != true &&
|
||||
(github.event_name == 'pull_request' || inputs.open_pr == true)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read # actual writes use a short-lived App token below
|
||||
id-token: write # SLSA provenance attestation
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Mint GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download all prebuild artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: ${{ runner.temp }}/dl
|
||||
pattern: ts-prebuild-*
|
||||
|
||||
- name: Place prebuilds, assert each built grammar has all 6, write SHA256SUMS
|
||||
id: place
|
||||
shell: bash
|
||||
env:
|
||||
MATRIX: ${{ needs.guard.outputs.matrix }}
|
||||
DL: ${{ runner.temp }}/dl
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module - <<'NODE'
|
||||
import fs from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
const include = JSON.parse(process.env.MATRIX).include;
|
||||
const dl = process.env.DL;
|
||||
const byGrammar = {};
|
||||
for (const e of include) (byGrammar[e.grammar] ||= { name: e.name, archs: [] }).archs.push(e.platform_arch);
|
||||
const PLATFORMS = ['linux-x64','linux-arm64','darwin-arm64','darwin-x64','win32-x64','win32-arm64'];
|
||||
const changed = [];
|
||||
for (const [grammar, { name }] of Object.entries(byGrammar)) {
|
||||
const dest = `gitnexus/vendor/${name}/prebuilds`;
|
||||
// A vendored grammar with 5/6 prebuilds silently breaks node-gyp-build
|
||||
// on the 6th platform — refuse a partial result.
|
||||
for (const pa of PLATFORMS) {
|
||||
const art = `${dl}/ts-prebuild-${grammar}-${pa}/${name}.node`;
|
||||
if (!fs.existsSync(art)) throw new Error(`missing ${grammar} prebuild for ${pa}`);
|
||||
fs.mkdirSync(`${dest}/${pa}`, { recursive: true });
|
||||
fs.copyFileSync(art, `${dest}/${pa}/${name}.node`);
|
||||
}
|
||||
execSync(`cd ${dest} && find . -name '*.node' | sort | xargs sha256sum > SHA256SUMS`);
|
||||
changed.push(name);
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `grammars=${changed.join(',')}\n`);
|
||||
console.log('Vendored prebuilds for:', changed.join(', '));
|
||||
NODE
|
||||
|
||||
- name: Attest build provenance (SLSA)
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
with:
|
||||
subject-path: 'gitnexus/vendor/tree-sitter-*/prebuilds/**/*.node'
|
||||
|
||||
- name: Create or update PR
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
GRAMMARS: ${{ steps.place.outputs.grammars }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { execSync } = require('node:child_process');
|
||||
const run = (c) => execSync(c, { stdio: ['ignore', 'pipe', 'inherit'] }).toString().trim();
|
||||
const grammars = process.env.GRAMMARS;
|
||||
const slug = grammars.replace(/[^a-z0-9]+/gi, '-');
|
||||
const branch = `chore/vendor-ts-prebuilds-${slug}-${context.runId}`;
|
||||
|
||||
run('git add gitnexus/vendor/tree-sitter-*/prebuilds');
|
||||
if (!run('git status --porcelain -- gitnexus/vendor/tree-sitter-*/prebuilds')) {
|
||||
core.notice('Prebuilds byte-identical to vendor; nothing to commit.');
|
||||
return;
|
||||
}
|
||||
run('git config user.name "gitnexus-release-bot[bot]"');
|
||||
run('git config user.email "gitnexus-release-bot[bot]@users.noreply.github.com"');
|
||||
run(`git checkout -b "${branch}"`);
|
||||
run(`git commit -m "chore(vendor): rebuild native prebuilds (${grammars})\n\nBuilt by ${process.env.RUN_URL}"`);
|
||||
const { owner, repo } = context.repo;
|
||||
const remote = `https://x-access-token:${process.env.GH_TOKEN}@github.com/${owner}/${repo}.git`;
|
||||
// Plain --force, not --force-with-lease: the branch is ephemeral and
|
||||
// unique per run (keyed by context.runId), written ONLY by this job, so
|
||||
// there is no concurrent writer to protect against. --force-with-lease
|
||||
// would compare against a remote-tracking ref this fresh checkout never
|
||||
// fetched, so re-running the SAME run (branch already pushed by attempt
|
||||
// 1) fails with "stale info" instead of overwriting.
|
||||
run(`git push --force "${remote}" "HEAD:${branch}"`);
|
||||
const body = [
|
||||
`Rebuilt the vendored native prebuilds for: **${grammars}**.`,
|
||||
'',
|
||||
`Builder run: ${process.env.RUN_URL}`,
|
||||
'Each `.node` was `require()`-loaded + parsed a real snippet on its target',
|
||||
'platform-arch before upload. SLSA build-provenance attested; `SHA256SUMS`',
|
||||
'committed alongside each grammar.',
|
||||
].join('\n');
|
||||
const { data: pr } = await github.rest.pulls.create({
|
||||
owner, repo, head: branch, base: 'main',
|
||||
title: `chore(vendor): tree-sitter prebuilds (${grammars})`, body,
|
||||
});
|
||||
core.info(`Opened PR #${pr.number}`);
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Scope Resolution Parity
|
||||
|
||||
# Reusable workflow — called from ci.yml. Does NOT declare concurrency;
|
||||
# it inherits the caller's concurrency group per the convention documented
|
||||
# in CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
|
||||
#
|
||||
# ── Purpose (RFC #909 Ring 3, §6.4 "Observability gates") ──────────────
|
||||
# For every language in `MIGRATED_LANGUAGES` (exported from
|
||||
# `gitnexus/src/core/ingestion/registry-primary-flag.ts`), run the
|
||||
# resolver integration test at `test/integration/resolvers/<slug>.test.ts`
|
||||
# TWICE on every PR:
|
||||
#
|
||||
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
|
||||
# broken the old path while migrating). Known legacy gaps may be skipped
|
||||
# through the resolver test helper's expected-failure list.
|
||||
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
|
||||
# new path carries the same behavior — the parity gate).
|
||||
#
|
||||
# BOTH must pass. The source of truth is the TypeScript constant — adding
|
||||
# a language to that `Set` is the ONLY contributor action; CI auto-
|
||||
# discovers it, runs parity, and the language's default production path
|
||||
# flips to registry-primary in the same change.
|
||||
#
|
||||
# When the set is empty (e.g. mid-Ring-3 for every language), the parity
|
||||
# matrix is skipped and the workflow reports success — no-op until a
|
||||
# language is explicitly claimed migrated.
|
||||
#
|
||||
# ── Consolidation (chore/vitest-speed-strategy) ────────────────────────
|
||||
# Previously each language was a separate GitHub Actions matrix job,
|
||||
# meaning N languages × 1 checkout+install+build per shard. The build
|
||||
# cost dwarfed the test cost (~5 min setup for ~15 sec test execution).
|
||||
#
|
||||
# Now a single job runs `scripts/run-parity.ts` which loops through all
|
||||
# migrated languages sequentially (2 vitest invocations per language:
|
||||
# legacy + registry-primary). All failures are collected and reported
|
||||
# at the end (equivalent to the old fail-fast: false behavior).
|
||||
#
|
||||
# Adding a new language to MIGRATED_LANGUAGES still requires no workflow
|
||||
# edit — the script auto-discovers the set at runtime.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
name: Discover migrated languages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
has-any: ${{ steps.read.outputs.has-any }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
|
||||
- name: Extract MIGRATED_LANGUAGES from registry-primary-flag.ts
|
||||
id: read
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
run: |
|
||||
set -euo pipefail
|
||||
LANGS=$(npx tsx scripts/ci-list-migrated-languages.ts)
|
||||
COUNT=$(printf '%s' "$LANGS" | jq 'length')
|
||||
HAS_ANY="false"
|
||||
if [[ "$COUNT" -gt 0 ]]; then HAS_ANY="true"; fi
|
||||
echo "has-any=$HAS_ANY" >> "$GITHUB_OUTPUT"
|
||||
echo "Discovered $COUNT migrated language(s): $LANGS"
|
||||
echo "Parity will run: $HAS_ANY"
|
||||
|
||||
parity:
|
||||
name: scope-resolution parity
|
||||
needs: discover
|
||||
if: needs.discover.outputs.has-any == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
with:
|
||||
build: 'true'
|
||||
|
||||
- name: Run parity for all migrated languages
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
run: npx tsx scripts/run-parity.ts
|
||||
@@ -94,9 +94,8 @@ jobs:
|
||||
# 1. Static, offline: assert every grammar's compiled ABI loads on the
|
||||
# pinned runtime (check-tree-sitter-upgrade-readiness.py --assert-current).
|
||||
# 2. Dynamic: run the parser-loader ABI load-smoke on the OS matrix so an
|
||||
# ABI-incompatible committed vendor prebuilt (e.g. Swift's — the static
|
||||
# check introspects source, not the shipped .node) fails on the platform
|
||||
# it ships to.
|
||||
# ABI-incompatible prebuilt (esp. the binary-only Swift vendor, which the
|
||||
# static check can't introspect) fails on the platform it ships to.
|
||||
abi-assert:
|
||||
name: tree-sitter ABI (${{ matrix.os }})
|
||||
strategy:
|
||||
|
||||
@@ -27,8 +27,9 @@ concurrency:
|
||||
# Each concern lives in its own workflow file for maintainability:
|
||||
# ci-quality.yml — typecheck (tsc --noEmit)
|
||||
# ci-tests.yml — unit + integration tests with coverage + cross-platform
|
||||
# (includes the scope-resolution resolver tests)
|
||||
# ci-e2e.yml — E2E tests (only when gitnexus-web/ changes)
|
||||
# ci-scope-parity.yml — RFC #909 Ring 3 parity gate: legacy DAG + registry-primary
|
||||
# both pass, per migrated language in the JSON registry
|
||||
#
|
||||
# Shared setup is DRY via .github/actions/setup-gitnexus composite action.
|
||||
|
||||
@@ -48,6 +49,11 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
scope-parity:
|
||||
uses: ./.github/workflows/ci-scope-parity.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# ── Save PR metadata for the reporting workflow ─────────────────
|
||||
# The ci-report.yml workflow (triggered by workflow_run) needs the
|
||||
# PR number and job results to post a comment. We save them as an
|
||||
@@ -56,7 +62,7 @@ jobs:
|
||||
save-pr-meta:
|
||||
name: Save PR Metadata
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
needs: [quality, tests, e2e]
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
@@ -67,12 +73,14 @@ jobs:
|
||||
QUALITY: ${{ needs.quality.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
mkdir -p pr-meta
|
||||
echo "$PR_NUMBER" > pr-meta/pr_number
|
||||
echo "$QUALITY" > pr-meta/quality_result
|
||||
echo "$TESTS" > pr-meta/tests_result
|
||||
echo "$E2E" > pr-meta/e2e_result
|
||||
echo "$SCOPE_PARITY" > pr-meta/scope_parity_result
|
||||
# TODO(post-merge): remove backward-compat copies once ci-report.yml
|
||||
# on main reads underscore names.
|
||||
# Backward-compat: ci-report.yml on main still reads hyphenated
|
||||
@@ -95,7 +103,7 @@ jobs:
|
||||
# Single required check for branch protection.
|
||||
ci-status:
|
||||
name: CI Gate
|
||||
needs: [quality, tests, e2e]
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
@@ -109,15 +117,14 @@ jobs:
|
||||
# reusable workflow, so `needs.tests.result` below blocks the merge
|
||||
# on an ABI mismatch. (`jobs.<id>.result` cannot be exposed as a
|
||||
# workflow_call output, so the gate is enforced transitively here.)
|
||||
# The scope-resolution resolver tests also run inside the `tests`
|
||||
# workflow (RING4-1 #942 removed the separate scope-parity gate),
|
||||
# so a resolver regression makes TESTS != success and blocks here.
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
echo "Quality: $QUALITY"
|
||||
echo "Tests: $TESTS"
|
||||
echo "E2E: $E2E"
|
||||
echo "Scope parity: $SCOPE_PARITY"
|
||||
# A failed `abi-assert` job (#1922) inside the tests reusable
|
||||
# workflow makes TESTS != success, so this clause also blocks the
|
||||
# merge on a tree-sitter ABI mismatch.
|
||||
@@ -130,3 +137,14 @@ jobs:
|
||||
echo "::error::E2E job failed"
|
||||
exit 1
|
||||
fi
|
||||
# scope-parity is a reusable workflow. With an empty migrated-
|
||||
# languages list, its parity matrix is skipped and the outer
|
||||
# workflow still reports `success`. If any entry's legacy-DAG or
|
||||
# registry-primary run fails, the workflow reports `failure`.
|
||||
# Accept only `success`; `skipped` would mean the entire
|
||||
# discover job was skipped too (upstream failure), which should
|
||||
# still block.
|
||||
if [[ "$SCOPE_PARITY" != "success" ]]; then
|
||||
echo "::error::Scope-resolution parity gate failed (RFC #909 Ring 3)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -38,18 +38,6 @@ jobs:
|
||||
# steps (and Gitleaks itself) don't need it for repo operations.
|
||||
persist-credentials: false
|
||||
|
||||
# gitleaks-action builds `base^..head` for pull_request events; both SHAs
|
||||
# must exist locally (fork PRs and shallow checkouts otherwise fail with
|
||||
# "unknown revision" — see gitleaks/gitleaks-action#199).
|
||||
- name: Fetch PR refs for gitleaks range
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
git fetch --no-tags origin "$BASE_SHA"
|
||||
git fetch --no-tags origin "$HEAD_SHA"
|
||||
|
||||
# No GITLEAKS_LICENSE secret is required for OSS / public-repo usage.
|
||||
# If this repo becomes private, the action will require a license key.
|
||||
- name: Gitleaks
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
name: Vendored grammar update monitor
|
||||
|
||||
# Periodically checks each vendored tree-sitter grammar against its
|
||||
# source-of-origin and opens a PR re-vendoring any update that is ABI-COMPATIBLE
|
||||
# with the pinned tree-sitter@0.21.1 (LANGUAGE_VERSION 13–14, #1922). The version
|
||||
# bump then triggers build-tree-sitter-prebuilds.yml, which cross-builds + ABI-
|
||||
# validates the prebuilds — so a re-vendor that is subtly wrong can never silently
|
||||
# ship: its PR's CI goes red.
|
||||
#
|
||||
# ABI-INCOMPATIBLE updates (the common case — upstreams move to newer tree-sitter)
|
||||
# are reported as a notice + job summary, NOT applied, so the monitor never opens
|
||||
# doomed PRs. tree-sitter-c is MONITORED but report-only: it is ABI-pinned at
|
||||
# 0.21.4 (#1242/#858), so an available c update is surfaced (notice + summary) but
|
||||
# never auto-bumped — a maintainer re-vendors it deliberately after a runtime
|
||||
# upgrade.
|
||||
#
|
||||
# Concurrency convention: see CONTRIBUTING.md -> "GitHub Actions — Concurrency Convention".
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 6 * * 1' # weekly, Monday 06:17 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
# Least privilege; the actual writes use a short-lived App token minted below.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
monitor:
|
||||
name: Check upstreams + open update PRs
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# secrets aren't usable in a job/step `if:`, so compute presence here.
|
||||
- name: Check release App secret
|
||||
id: relapp
|
||||
env:
|
||||
HAS_APP: ${{ secrets.RELEASE_APP_ID != '' && secrets.RELEASE_APP_PRIVATE_KEY != '' }}
|
||||
run: echo "configured=$HAS_APP" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Mint GitHub App token
|
||||
id: app-token
|
||||
if: steps.relapp.outputs.configured == 'true'
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Detect updates, re-vendor ABI-compatible ones, open PRs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
HAS_APP: ${{ steps.relapp.outputs.configured }}
|
||||
# App token writes; falls back to the read-only job token (PRs then skip).
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || github.token }}
|
||||
script: |
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const SCRIPT = '.github/scripts/update-vendored-grammars.mjs';
|
||||
const run = (cmd, args, opts = {}) =>
|
||||
execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts });
|
||||
|
||||
const report = JSON.parse(run('node', [SCRIPT]));
|
||||
const { owner, repo } = context.repo;
|
||||
const hasApp = process.env.HAS_APP === 'true';
|
||||
const applied = [], held = [], errors = [], skipped = [];
|
||||
|
||||
run('git', ['config', 'user.name', 'gitnexus-release-bot[bot]']);
|
||||
run('git', ['config', 'user.email', 'gitnexus-release-bot[bot]@users.noreply.github.com']);
|
||||
const baseSha = run('git', ['rev-parse', 'HEAD']).trim();
|
||||
|
||||
for (const r of report) {
|
||||
if (r.error) { errors.push(r); continue; }
|
||||
if (!r.update) continue;
|
||||
if (!r.applicable) { held.push(r); continue; } // ABI-incompatible / unknown
|
||||
|
||||
const name = `tree-sitter-${r.grammar}`;
|
||||
const branch = `chore/update-${name}-${r.upstream}`.replace(/[^a-z0-9._/-]+/gi, '-');
|
||||
|
||||
// Idempotency: don't reopen an existing PR for this exact version.
|
||||
const existing = await github.rest.pulls.list({ owner, repo, head: `${owner}:${branch}`, state: 'all' });
|
||||
if (existing.data.length > 0) { skipped.push({ ...r, reason: 'PR exists' }); continue; }
|
||||
|
||||
// Re-vendor in place (refuses + exits non-zero if ABI turns out wrong).
|
||||
try {
|
||||
run('node', [SCRIPT, '--apply', r.grammar]);
|
||||
} catch (e) {
|
||||
errors.push({ ...r, error: `apply failed: ${String(e.message || e).slice(0, 200)}` });
|
||||
run('git', ['checkout', '--', 'gitnexus/vendor']);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasApp) {
|
||||
skipped.push({ ...r, reason: 'no RELEASE_APP secret — PR not opened' });
|
||||
run('git', ['checkout', '--', 'gitnexus/vendor']);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remote = `https://x-access-token:${process.env.GH_TOKEN}@github.com/${owner}/${repo}.git`;
|
||||
run('git', ['checkout', '-B', branch, baseSha]);
|
||||
run('git', ['add', `gitnexus/vendor/${name}`]);
|
||||
run('git', ['commit', '-m', `chore(vendor): update ${name} to ${r.upstream}`]);
|
||||
run('git', ['push', '--force-with-lease', remote, `HEAD:${branch}`]);
|
||||
const body = [
|
||||
`Automated re-vendor of **${name}** to \`${r.upstream}\` (from ${r.kind === 'npm' ? `npm \`${name}\`` : `\`${r.ref}\``}).`,
|
||||
'',
|
||||
`Verified ABI **${r.abi}** — compatible with the pinned \`tree-sitter@0.21.1\` (13–14).`,
|
||||
'Source-build inputs refreshed; the GitNexus binding.gyp / README / prebuilds are preserved.',
|
||||
'The version bump triggers `build-tree-sitter-prebuilds.yml` to rebuild + ABI-validate the',
|
||||
'prebuilds — review its result before merging.',
|
||||
].join('\n');
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner, repo, head: branch, base: 'main',
|
||||
title: `chore(vendor): update ${name} to ${r.upstream}`, body,
|
||||
});
|
||||
applied.push({ ...r, pr: pr.data.number });
|
||||
run('git', ['checkout', '--force', baseSha]);
|
||||
}
|
||||
|
||||
// Summary
|
||||
const s = core.summary.addHeading('Vendored grammar update monitor');
|
||||
if (applied.length) s.addRaw(`\n**Opened PRs:** ${applied.map((a) => `${a.grammar}→${a.upstream} (#${a.pr})`).join(', ')}\n`);
|
||||
if (held.length) s.addRaw(`\n**Held (not auto-applied):** ${held.map((h) => `${h.grammar} ${h.upstream} (${h.hold ? 'report-only: ' + h.hold : 'ABI ' + (h.abi ?? '?') + ' — needs the tree-sitter runtime upgrade'})`).join(', ')}\n`);
|
||||
if (skipped.length) s.addRaw(`\n**Skipped:** ${skipped.map((x) => `${x.grammar} (${x.reason})`).join(', ')}\n`);
|
||||
if (errors.length) s.addRaw(`\n**Errors:** ${errors.map((e) => `${e.grammar}: ${e.error}`).join('; ')}\n`);
|
||||
if (!applied.length && !held.length && !skipped.length && !errors.length) s.addRaw('\nAll vendored grammars are up to date. ✅\n');
|
||||
await s.write();
|
||||
|
||||
for (const h of held) core.notice(`${h.grammar}: update to ${h.upstream} available — ${h.hold ? `report-only (${h.hold})` : `ABI ${h.abi ?? 'unknown'} (need 13/14), held until the tree-sitter runtime upgrade`}.`);
|
||||
if (!hasApp && (applied.length || skipped.some((x) => /secret/.test(x.reason)))) {
|
||||
core.notice('RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY not configured — update PRs were not opened. Provision the App to enable auto-PRs.');
|
||||
}
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
# ── Phase 3: reusable CI gate ──────────────────────────────────────────────
|
||||
# Runs for both rc (when guard says go) and stable. No `secrets:` passed —
|
||||
# ci.yml and its entire reusable-workflow chain (ci-quality, ci-tests,
|
||||
# ci-e2e, ci-report) reference zero `secrets.*` values;
|
||||
# ci-e2e, ci-scope-parity, ci-report) reference zero `secrets.*` values;
|
||||
# passing any would be unused surface. GITHUB_TOKEN is implicit.
|
||||
ci:
|
||||
needs: [route, rc-guard]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
title = "GitNexus"
|
||||
|
||||
[extend]
|
||||
useDefault = true
|
||||
|
||||
# Fake embedding API keys in unit tests (current probe + historical placeholder).
|
||||
[allowlist]
|
||||
description = "fake embedding API keys in http-embedder unit tests"
|
||||
regexes = [
|
||||
'''secret-key-12345''',
|
||||
'''test-api-key-redaction-check''',
|
||||
]
|
||||
@@ -39,7 +39,8 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
|
||||
## Reference docs
|
||||
|
||||
- **[ARCHITECTURE.md](ARCHITECTURE.md)**, **[CONTRIBUTING.md](CONTRIBUTING.md)**, **[GUARDRAILS.md](GUARDRAILS.md)**
|
||||
- **Call & inheritance resolution (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. All languages resolve calls and inheritance through the scope-resolution pipeline (`Registry.lookup`, `preEmitInheritanceEdges`, `emitHeritageEdges`, `buildMro` → `MethodDispatchIndex`). **Shared code in `gitnexus/src/core/ingestion/` must not name languages** — plug language behavior in via `LanguageProvider` / `ScopeResolver` hooks. A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. (The legacy call-resolution DAG + `@heritage` capture path were removed in RING4-1 #942.)
|
||||
- **Call-resolution DAG (legacy path):** See ARCHITECTURE.md § Call-Resolution DAG. Typed 6-stage DAG inside the `parse` phase; language-specific behavior behind `inferImplicitReceiver` / `selectDispatch` hooks on `LanguageProvider`. Shared code in `gitnexus/src/core/ingestion/` must not name languages. Types: `gitnexus/src/core/ingestion/call-types.ts`.
|
||||
- **Scope-resolution pipeline (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. Replaces the legacy DAG for languages in `MIGRATED_LANGUAGES` (see `registry-primary-flag.ts`). A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. CI parity gate runs BOTH paths per migrated language on every PR.
|
||||
- **Cursor:** `.cursor/index.mdc` (always-on); `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` deprecated.
|
||||
- **GitNexus:** skills in `.claude/skills/gitnexus/`; MCP rules in `gitnexus:start` block below.
|
||||
|
||||
@@ -80,18 +81,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 `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 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 warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- 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"})`.
|
||||
- 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"})`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `impact` on it.
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -173,6 +174,6 @@ npx gitnexus serve # HTTP API on port 4747 (from any ind
|
||||
|
||||
### Gotchas
|
||||
|
||||
- `npm install` in `gitnexus/` triggers `prepare` (builds via `tsc`) and `postinstall` (materializes the vendored grammars into `node_modules/`, then prefers a committed prebuild per platform-arch and only source-builds when none matches). A C/C++ toolchain (`python3`, `make`, `g++`) is needed only for that source-build fallback.
|
||||
- The vendored grammars `tree-sitter-{c,dart,proto,swift,kotlin}` are handled uniformly: c is required; dart/proto/swift/kotlin are optional and skippable via `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1`. Install warnings appear only when no prebuild matches the platform-arch and no toolchain is present, and are non-fatal — only that language's parsing is unavailable.
|
||||
- `npm install` in `gitnexus/` triggers `prepare` (builds via `tsc`) and `postinstall` (patches tree-sitter-swift, builds tree-sitter-proto). Native bindings need `python3`, `make`, `g++`.
|
||||
- `tree-sitter-kotlin` and `tree-sitter-swift` are optional — install warnings expected.
|
||||
- ESLint configured via `eslint.config.mjs` (TS, React Hooks, unused-imports). No `npm run lint` script; use `npx eslint .`. Prettier runs via lint-staged. CI checks both in `ci-quality.yml`.
|
||||
|
||||
+114
-31
@@ -15,7 +15,7 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
|
||||
|
||||
## End-to-end flow: index → graph → tools
|
||||
|
||||
1. **Ingestion** — `analyze.ts` → `runFullAnalysis` (`run-analyze.ts`) → `runPipelineFromRepo` (`pipeline.ts`). DAG of 14 phases builds a `KnowledgeGraph` in memory, then loads into LadybugDB under `.gitnexus/`. Repo registered in `~/.gitnexus/registry.json` for MCP discovery.
|
||||
1. **Ingestion** — `analyze.ts` → `runFullAnalysis` (`run-analyze.ts`) → `runPipelineFromRepo` (`pipeline.ts`). DAG of 12 phases builds a `KnowledgeGraph` in memory, then loads into LadybugDB under `.gitnexus/`. Repo registered in `~/.gitnexus/registry.json` for MCP discovery.
|
||||
|
||||
2. **Persistence** — `repo-manager.ts` (paths, registry, KuzuDB cleanup). `lbug-adapter.ts` (graph load, queries, embedding batches).
|
||||
|
||||
@@ -65,7 +65,7 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
|
||||
| Wiki generation | `src/core/wiki/` |
|
||||
| Language support | `src/core/ingestion/languages/` + `tree-sitter-queries.ts` + `gitnexus-shared/src/languages.ts` |
|
||||
| Import resolution | `src/core/ingestion/import-processor.ts` + `import-resolvers/configs/` + `model/resolution-context.ts` |
|
||||
| Call resolution/inheritance/MRO | `src/core/ingestion/scope-resolution/` (pipeline, passes, graph-bridge) |
|
||||
| Call resolution/MRO | `src/core/ingestion/call-processor.ts` + `model/resolve.ts` |
|
||||
| Type extraction | `src/core/ingestion/type-extractors/` |
|
||||
| Worker pool | `src/core/ingestion/workers/` |
|
||||
| Web UI | `gitnexus-web/src/` |
|
||||
@@ -77,11 +77,11 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
|
||||
|
||||
## Pipeline Phase DAG
|
||||
|
||||
14 phases defined in `gitnexus/src/core/ingestion/pipeline-phases/`, each with explicit `deps` and typed output.
|
||||
12 phases defined in `gitnexus/src/core/ingestion/pipeline-phases/`, each with explicit `deps` and typed output.
|
||||
|
||||
```
|
||||
scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
|
||||
→ crossFile → scopeResolution → pruneLocalSymbols → mro → communities → processes
|
||||
→ crossFile → mro → communities → processes
|
||||
```
|
||||
|
||||
| Phase | File | Deps | Output |
|
||||
@@ -95,13 +95,11 @@ scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
|
||||
| `tools` | `tools.ts` | `parse` | Tool nodes + HANDLES_TOOL edges |
|
||||
| `orm` | `orm.ts` | `parse` | QUERIES edges (Prisma, Supabase) |
|
||||
| `crossFile` | `cross-file.ts` + `cross-file-impl.ts` | `parse`, `routes`, `tools`, `orm` | Cross-file type propagation in topological import order |
|
||||
| `scopeResolution` | `scope-resolution/pipeline/phase.ts` | `parse`, `crossFile`, `structure` | Binding/reference + inheritance edges; disposes BindingAccumulator |
|
||||
| `pruneLocalSymbols` | `prune-local-symbols.ts` | `scopeResolution` | Drops inert block-local `Const`/`Variable`/`Static` nodes (only a `File→DEFINES` edge) post-resolution |
|
||||
| `mro` | `mro.ts` | `crossFile`, `scopeResolution`, `pruneLocalSymbols`, `structure` | METHOD_OVERRIDES + METHOD_IMPLEMENTS edges |
|
||||
| `communities` | `communities.ts` | `mro`, `pruneLocalSymbols`, `structure` | Community nodes + MEMBER_OF edges (Leiden algorithm) |
|
||||
| `processes` | `processes.ts` | `communities`, `routes`, `tools`, `pruneLocalSymbols`, `structure` | Process nodes + STEP_IN_PROCESS edges |
|
||||
| `mro` | `mro.ts` | `crossFile`, `structure` | METHOD_OVERRIDES + METHOD_IMPLEMENTS edges |
|
||||
| `communities` | `communities.ts` | `mro`, `structure` | Community nodes + MEMBER_OF edges (Leiden algorithm) |
|
||||
| `processes` | `processes.ts` | `communities`, `routes`, `tools`, `structure` | Process nodes + STEP_IN_PROCESS edges |
|
||||
|
||||
**Non-phase files in the same directory:** `parse-impl.ts`, `cross-file-impl.ts` (implementation), `wildcard-synthesis.ts` (whole-module import expansion), `types.ts`, `runner.ts`, `index.ts`.
|
||||
**Non-phase files in the same directory:** `parse-impl.ts`, `cross-file-impl.ts` (implementation), `wildcard-synthesis.ts` (whole-module import expansion), `orm-extraction.ts` (sequential ORM fallback), `types.ts`, `runner.ts`, `index.ts`.
|
||||
|
||||
### DAG runner
|
||||
|
||||
@@ -121,8 +119,7 @@ scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
|
||||
- **Single graph accumulator** — all phases mutate the same `KnowledgeGraph` in `ctx`; the graph is the primary output.
|
||||
- **Typed phase access** — `getPhaseOutput<T>(deps, 'name')` for type-safe upstream results.
|
||||
- **Binding accumulator lifecycle** — created in `parse`, disposed by `crossFile` (in `finally`). No other phase should take ownership.
|
||||
- **Skippable phases** — `skipGraphPhases` omits MRO/communities/processes (faster tests); `pruneLocalSymbols` still runs (it is graph cleanup, not analysis). `skipWorkers` is no longer a sequential escape hatch — it (like `--workers 0` / `GITNEXUS_WORKER_POOL_SIZE=0`) is rejected with an actionable error, since the worker pool is the sole parse path (§ Chunked parse-and-resolve).
|
||||
- **Local-symbol pruning** — `pruneLocalSymbols` removes inert block-local value symbols after scope resolution has consumed them. Opt out per-call with `PipelineOptions.keepLocalValueSymbols` or globally with the `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` env var.
|
||||
- **Skippable phases** — `skipGraphPhases` omits MRO/communities/processes (faster tests). `skipWorkers` forces sequential parsing.
|
||||
|
||||
### How to add a new phase
|
||||
|
||||
@@ -150,18 +147,105 @@ export const myPhase: PipelinePhase<MyPhaseOutput> = {
|
||||
|
||||
---
|
||||
|
||||
## Semantic model
|
||||
## Call-Resolution DAG
|
||||
|
||||
`SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). The scope-resolution pipeline reads from here: `findOwnedMember`, `pickOverload`, and `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
|
||||
Typed 6-stage pipeline in `call-processor.ts` (inside the `parse` phase) that resolves method/function calls and emits CALLS edges. Language behavior plugs in at two `LanguageProvider` hook points (stages 3–4); shared code names no languages. Scope: call resolution only — import resolution, type extraction, heritage, and symbol-table population live in other phases.
|
||||
|
||||
`ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact the scope-resolution pipeline consumes. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
|
||||
### Stages
|
||||
|
||||
```
|
||||
extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
|
||||
(1) (2) (3) [hook] (4) [hook] (5) (6)
|
||||
```
|
||||
|
||||
| Stage | Produces | Location |
|
||||
|-------|----------|----------|
|
||||
| **extract-call** | `ExtractedCallSite` (name, form, receiver, argCount) | `call-extractors/` (per-language); runs in worker |
|
||||
| **classify-form** | callForm (`free`/`member`/`constructor`) + arity | `call-analysis.ts` → `inferCallForm`; shared, runs in worker |
|
||||
| **infer-receiver** | `ReceiverEnriched` (receiver type finalized) | `call-processor.ts`; shared default chain, then `inferImplicitReceiver` hook |
|
||||
| **select-dispatch** | `DispatchDecision` (primary, fallback, ancestryView) | `selectDispatch` hook, falls back to shared default |
|
||||
| **resolve-target** | `TieredCandidates` | `model/resolve.ts` → `lookupMethodByOwnerWithMRO` (MRO walk) |
|
||||
| **emit-edge** | CALLS edge in graph | `call-processor.ts`; writes edge with confidence tier |
|
||||
|
||||
### Provider hooks
|
||||
|
||||
Both hooks are optional on `LanguageProvider`. Ruby is the only current implementer.
|
||||
|
||||
**`inferImplicitReceiver`** — called after shared infer-receiver defaults. Returns `ImplicitReceiverOverride | null`.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `callNode` (AST), `filePath` |
|
||||
| Non-null fields | `callForm`, `receiverName`, `receiverTypeName` (required); `receiverSource: 'implicit-self'` (fixed); `hint?` (opaque, passed to `selectDispatch`) |
|
||||
| Null | Keep existing `ReceiverEnriched` state |
|
||||
|
||||
**`selectDispatch`** — called after infer-receiver (including hook). Returns `DispatchDecision | null`; null uses shared default (constructor → `primary:'constructor'`; typed receiver → `primary:'owner-scoped'`; else → `primary:'free'`).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `receiverSource`, `hint` |
|
||||
| Non-null fields | `primary: 'owner-scoped' \| 'free' \| 'constructor'`; `fallback?: 'free-arity-narrowed'`; `ancestryView?: 'instance' \| 'singleton'`; `hint?` |
|
||||
|
||||
**`DispatchDecision` field semantics:**
|
||||
- `primary: 'owner-scoped'` — MRO walk from receiver's type; used when receiver type is known.
|
||||
- `fallback: 'free-arity-narrowed'` — after owner-scoped miss, search free-call candidates by arity only (Ruby uses this for implicit-self calls that miss their owner's MRO).
|
||||
- `ancestryView: 'singleton'` — walk singleton/class ancestry instead of instance ancestry (Ruby `def self.foo` bodies, so `extend`-ed methods are found).
|
||||
|
||||
### Adding language behavior
|
||||
|
||||
1. **Implicit receivers** — implement `inferImplicitReceiver`: return null if call already has a receiver; otherwise use `findEnclosingClassInfo` (`ast-helpers.ts`) to find the enclosing context, return `ImplicitReceiverOverride` with `receiverSource: 'implicit-self'`, and optionally set `hint` for `selectDispatch`.
|
||||
2. **Custom dispatch** — implement `selectDispatch`: inspect `receiverSource` and `hint`, return `DispatchDecision` with `primary`, optional `fallback`, optional `ancestryView`; return null to keep shared defaults.
|
||||
3. **MRO strategy** — confirm `mroStrategy` is `'first-wins'`, `'c3'`, `'ruby-mixin'`, or `'none'`; consumed by `lookupMethodByOwnerWithMRO`.
|
||||
|
||||
**Ruby example** (`languages/ruby.ts` + `utils/ruby-self-call.ts`): `inferImplicitReceiver` rewrites bare-identifier calls to `self.method` and sets `hint` to `'instance'`/`'singleton'`; `selectDispatch` uses hint for `ancestryView` and adds `fallback: 'free-arity-narrowed'` for implicit-self calls.
|
||||
|
||||
### Code references
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `core/ingestion/call-types.ts` | DAG types: `ReceiverEnriched`, `DispatchDecision`, `ImplicitReceiverOverride` |
|
||||
| `core/ingestion/language-provider.ts` | Hook signatures: `inferImplicitReceiver`, `selectDispatch` |
|
||||
| `core/ingestion/call-processor.ts` | `processCalls`: stages 3–6 |
|
||||
| `core/ingestion/model/resolve.ts` | `lookupMethodByOwnerWithMRO`: stage 5 MRO walk |
|
||||
| `core/ingestion/languages/ruby.ts` | Both hooks + `mroStrategy: 'ruby-mixin'` |
|
||||
| `core/ingestion/utils/ruby-self-call.ts` | Bare-call rewrite for `inferImplicitReceiver` |
|
||||
|
||||
### Coexistence with the scope-resolution pipeline
|
||||
|
||||
The Call-Resolution DAG is the **legacy path**. RFC #909 Ring 3 introduces a parallel **scope-resolution pipeline** (next section) that replaces stages 1–6 with a scope-indexed registry lookup. Both paths ship side-by-side and are gated per-language via `MIGRATED_LANGUAGES` + the `REGISTRY_PRIMARY_<LANG>` env var.
|
||||
|
||||
- **Unmigrated language** → Call-Resolution DAG runs; scope-resolution phase is a no-op.
|
||||
- **Migrated language** (currently: Python, C#) → scope-resolution owns CALLS/ACCESSES/USES emission; the legacy DAG gates off for that language via `isRegistryPrimary(lang)` checks in `call-processor.ts` and `import-processor.ts`.
|
||||
- `import-processor` still populates `importMap` for migrated languages — heritage's `ctx.resolve` reads it to disambiguate parent classes. Only edge emission is gated.
|
||||
- CI runs BOTH paths for every migrated language on every PR (`.github/workflows/ci-scope-parity.yml`); both must pass.
|
||||
|
||||
#### Same-graph guarantee
|
||||
|
||||
Edges emitted by the scope-resolution pipeline and edges emitted by the legacy DAG are indistinguishable to downstream consumers (MCP tools, HTTP API, embeddings, group bridge):
|
||||
|
||||
- **Node identity** — both paths use `generateId(...)` from `lib/utils.ts`, the same qualified-name keyspace, and the same node labels (`File`, `Folder`, `Class`, `Method`, `Function`, …). Overload disambiguation suffixes `parameterTypes` into the id consistently — see `scope-resolution/graph-bridge/ids.ts` and the legacy emitter in `call-processor.ts`.
|
||||
- **Edge vocabulary** — both paths emit the same reasons: `'import-resolved' | 'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read' | 'write'`. Migrating a language must not change which reasons consumers see for previously-resolved edges.
|
||||
- **Confidence tier** — both paths attach a numeric `confidence` to each edge using the same scale.
|
||||
|
||||
The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) runs both paths against every migrated language's fixture corpus and fails on any divergence.
|
||||
|
||||
#### Semantic-model source of truth
|
||||
|
||||
Two independent invariants.
|
||||
|
||||
**ParsedFile = the AST-level truth.** `ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact both resolution paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
|
||||
|
||||
**SemanticModel = the symbol-level truth.** `SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). Both paths read from here:
|
||||
|
||||
- Legacy Call-Resolution DAG → `call-processor` Tier 1/2/3 via `model.symbols.lookupExactAll`, `model.methods.lookupMethodByName`, `model.types.lookupClassByName`, `lookupMethodByOwnerWithMRO`.
|
||||
- Scope-resolution pipeline → `findOwnedMember`, `pickOverload`, `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
|
||||
|
||||
The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` for `Scope`-valued lookups (`classScopeByDefId`, `moduleScopeByFile`) that `SemanticModel` structurally cannot hold. No symbol-indexed duplicates exist outside `SemanticModel`.
|
||||
|
||||
**Write / read phase contract.** The model is mutable during three ordered phases and read-only afterward:
|
||||
|
||||
```
|
||||
Phase 1: parse ──► symbolTable.add fans into types/methods/fields
|
||||
Phase 1: legacy parse ──► symbolTable.add fans into types/methods/fields
|
||||
Phase 2: scope-resolution ──► reconcileOwnership() registers corrected ownerIds
|
||||
Phase 3: finalize ──► model.attachScopeIndexes(bundle) — one-shot freeze
|
||||
─────────────────────────── phase boundary ───────────────────────────
|
||||
@@ -171,7 +255,7 @@ The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` fo
|
||||
|
||||
`runScopeResolution` narrows `MutableSemanticModel` → `SemanticModel` at the phase boundary so downstream passes physically cannot mutate the model even accidentally.
|
||||
|
||||
**Reconciliation pass.** `reconcileOwnership` (`scope-resolution/pipeline/reconcile-ownership.ts`) is a shim for languages whose parse-time extractor doesn't resolve `enclosingClassId` at parse time (Python class-body methods are the canonical case). It walks `parsed.localDefs[i].ownerId` after `populateOwners` and registers any missed methods/fields into the model. Idempotent — safe to re-run, safe alongside languages whose extractor already carries `ownerId` (C#).
|
||||
**Transitional: reconciliation pass.** `reconcileOwnership` (`scope-resolution/pipeline/reconcile-ownership.ts`) is a shim for languages whose legacy extractor doesn't resolve `enclosingClassId` at parse time (Python class-body methods are the canonical case). It walks `parsed.localDefs[i].ownerId` after `populateOwners` and registers any missed methods/fields into the model. Idempotent — safe to re-run, safe alongside languages whose legacy extractor already carries `ownerId` (C#).
|
||||
|
||||
The architectural end state is for every language's parse-time extractor to emit the correct `ownerId` directly, making reconciliation a no-op (tracked as a follow-up refactor). The dev-mode validator `validateOwnershipParity` surfaces any drift via `onWarn` under `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`.
|
||||
|
||||
@@ -181,7 +265,7 @@ References: `semantic-model.ts` file-head (full write/read contract); `contract/
|
||||
|
||||
## Scope-Resolution Pipeline (RFC #909 Ring 3)
|
||||
|
||||
Language-agnostic scope-resolution resolver. This is the resolution path for every language — it owns CALLS/ACCESSES/USES emission and inheritance edges. Adding a language is one interface implementation (`ScopeResolver`) plus one registration in the `SCOPE_RESOLVERS` map — no changes to shared code, no new pipeline phase. (RING4-1 #942 removed the legacy call-resolution DAG and the per-language `MIGRATED_LANGUAGES` flag, so `SCOPE_RESOLVERS` registration is all that's needed.)
|
||||
Language-agnostic registry-primary resolver. Replaces the Call-Resolution DAG for migrated languages. Adding a language is one interface implementation (`ScopeResolver`) plus two registrations — no changes to shared code, no new pipeline phase.
|
||||
|
||||
### Pipeline stages
|
||||
|
||||
@@ -202,7 +286,7 @@ Language-agnostic scope-resolution resolver. This is the resolution path for eve
|
||||
```
|
||||
|
||||
Orchestrator: `runScopeResolution(input, provider)` in `scope-resolution/pipeline/run.ts`.
|
||||
Pipeline phase: `scopeResolutionPhase` in `scope-resolution/pipeline/phase.ts` — iterates the registered `SCOPE_RESOLVERS` over the worker-serialized `ParsedFile`s. (Per-language `emitScopeCaptures` hooks may reuse a cached Tree via the orchestrator's `treeCache`, but in worker-pool runs that cache is empty — Trees can't cross MessageChannels — so they consume the pre-extracted `ParsedFile` instead; § Performance notes.)
|
||||
Pipeline phase: `scopeResolutionPhase` in `scope-resolution/pipeline/phase.ts` — iterates `SCOPE_RESOLVERS ∩ MIGRATED_LANGUAGES`, reads per-file Trees from the parse phase's `scopeTreeCache`, disposes the cache at the end.
|
||||
|
||||
### `ScopeResolver` contract
|
||||
|
||||
@@ -228,6 +312,7 @@ Single interface a language implements to plug into the pipeline. Contract fully
|
||||
|
||||
1. Implement `ScopeResolver` in `languages/<lang>/scope-resolver.ts`.
|
||||
2. Add entry to `SCOPE_RESOLVERS` in `scope-resolution/pipeline/registry.ts`.
|
||||
3. Add the language to `MIGRATED_LANGUAGES` in `registry-primary-flag.ts` when the shadow-harness corpus parity ≥ 99% fixtures / ≥ 98% corpus.
|
||||
|
||||
CI auto-discovers the set via `tsx`. No workflow edit required.
|
||||
|
||||
@@ -243,6 +328,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
|
||||
| `scope-resolution/graph-bridge/*.ts` | CLI-local translation from resolved references → `KnowledgeGraph` edges |
|
||||
| `scope-resolution/scope/*.ts` | Generic scope-chain walkers + namespace targets |
|
||||
| `scope-resolution/workspace-index.ts` | Build-once O(1) lookup index |
|
||||
| `registry-primary-flag.ts` | `MIGRATED_LANGUAGES` set + `isRegistryPrimary(lang)` |
|
||||
| `languages/python/index.ts` | Python `ScopeResolver` hooks + known-limitation docs |
|
||||
| `languages/python/captures.ts` | `emitPythonScopeCaptures` (honors cross-phase Tree cache) |
|
||||
| `languages/csharp/index.ts` | C# `ScopeResolver` hooks + known-limitation docs |
|
||||
@@ -251,7 +337,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
|
||||
|
||||
### Performance notes
|
||||
|
||||
- **Cross-phase Tree cache**: the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) lets a scope-resolution per-language hook (`emitScopeCaptures`) reuse a tree instead of re-parsing. Workers leave it empty — Trees can't cross MessageChannels — so in normal (worker-pool) runs scope-resolution does NOT rely on it: workers serialize each file's `ParsedFile` (+ capture side-channel) and stream them in, so scope-resolution consumes the pre-extracted artifact rather than re-parsing on the main thread (§ Chunked parse-and-resolve). `PROF_SCOPE_RESOLUTION=1` emits hit/miss counters and a worker-engaged warning.
|
||||
- **Cross-phase Tree cache**: parse phase writes Trees into `scopeTreeCache` (separate from the chunk-local `astCache`) ONLY for languages with `emitScopeCaptures`. Scope-resolution reads from it to skip the second parse. Cleared at end of the phase. Workers leave the cache empty — Trees can't cross MessageChannels; cache miss = fresh parse. `PROF_SCOPE_RESOLUTION=1` emits hit/miss counters and a worker-engaged warning.
|
||||
- **Typed relationship iteration**: heritage + MRO walk only the EXTENDS / IMPLEMENTS / HAS_METHOD edges via `iterRelationshipsByType`, not the full relationship map.
|
||||
- **Workspace-resolution-index**: O(1) `findOwnedMember` / `findExportedDef` / `classScopeByDefId` built once per run.
|
||||
- **SCC-ordered cross-file return-type propagation** (PR #1050): `propagateImportedReturnTypes` walks `indexes.sccs` in reverse-topological order (leaves first), so multi-hop alias chains like `models.User → service.user → app.user` collapse to the terminal class in a single linear pass. Within each importer, the source module's `typeBindings` is chain-followed BEFORE mirroring (so we mirror terminal types, not intermediate refs), and the importer's own `typeBindings` is chain-followed AFTER mirroring (so local `const x = importedFn()` resolves before downstream importers run). Cyclic SCCs reach a partial fixpoint within a single pass without iterating to convergence — see the `ts-circular` cross-file-binding fixture which only asserts pipeline-no-throw. PROF output (`PROF_SCOPE_RESOLUTION=1`) splits `finalize` from `propagate` so quadratic regressions in the chain-follow surface independently.
|
||||
@@ -265,7 +351,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
|
||||
```
|
||||
Unified Graph Schema (44 node types, 21 relationship types)
|
||||
↑
|
||||
Scope-Resolution Pipeline (registry lookup + 3-tier import resolution + MRO)
|
||||
Unified Resolution (3-tier name lookup + MRO walk)
|
||||
↑
|
||||
Language Providers (import semantics, type config, export checker, MRO strategy)
|
||||
↑
|
||||
@@ -290,7 +376,7 @@ Each language implements `LanguageProvider` (`language-provider.ts`). Key fields
|
||||
|
||||
### Unified capture tags
|
||||
|
||||
Per-language tree-sitter queries use different AST node names but produce the **same semantic capture tags**: `@definition.class`, `@definition.function`, `@call.name`, `@import.source`, `@reference.inherits`. Downstream extraction needs no language branching. Defined in `tree-sitter-queries.ts`.
|
||||
Per-language tree-sitter queries use different AST node names but produce the **same semantic capture tags**: `@definition.class`, `@definition.function`, `@call.name`, `@import.source`, `@heritage.extends`. Downstream extraction needs no language branching. Defined in `tree-sitter-queries.ts`.
|
||||
|
||||
### Import resolution
|
||||
|
||||
@@ -314,26 +400,23 @@ Unified 3-tier algorithm (`model/resolution-context.ts`), per-language `importSe
|
||||
### Chunked parse-and-resolve
|
||||
|
||||
`parse` processes files in ~20 MB byte-budget chunks to bound memory. Per chunk:
|
||||
1. Worker pool dispatches files (the sole parse path — there is no sequential fallback; `skipWorkers`, `--workers 0`, and `GITNEXUS_WORKER_POOL_SIZE=0` are rejected with an actionable error)
|
||||
1. Worker pool dispatches files (or sequential fallback via `skipWorkers`)
|
||||
2. Each worker: detect language → load grammar → run queries → return unified `ParseWorkerResult`
|
||||
3. Synthesize wildcard bindings (`wildcard-synthesis.ts`)
|
||||
4. Resolve imports
|
||||
4. Resolve imports and heritage
|
||||
5. Collect `BindingAccumulator` entries for cross-file propagation
|
||||
|
||||
Inheritance edges are emitted later, by the scope-resolution phase (`preEmitInheritanceEdges` + `emitHeritageEdges`), not during `parse`.
|
||||
|
||||
Workers: `workers/worker-pool.ts`, `workers/parse-worker.ts`.
|
||||
|
||||
**Worker-serialized ParsedFiles (#2038).** To index very large repos (e.g. the Linux kernel) without OOM, the worker pool is the *sole* parse path and workers serialize each file's `ParsedFile` (plus its capture side-channel) in parallel, streaming them to scope-resolution through a disk-backed store. Scope-resolution consumes the pre-extracted artifact instead of re-parsing every file on the main thread — tree-sitter's native input buffers are not GC-reclaimable, so the former main-thread re-parse leaked native memory until the process died. Pool creation is lazy / cache-miss-gated, so a warm all-cache-hit run replays cached worker output without spawning a worker (hence `usedWorkerPool` can be false even when the repo has parseable files).
|
||||
### Heritage and MRO
|
||||
|
||||
### Inheritance and MRO
|
||||
|
||||
Inheritance is captured by the `@reference.inherits` tag and emitted by the scope-resolution phase: `preEmitInheritanceEdges` resolves each base in scope, then `emitHeritageEdges` writes the `EXTENDS`/`IMPLEMENTS` edges. The phase then computes method resolution order via each `ScopeResolver`'s `buildMro` hook, feeding a `MethodDispatchIndex` used for owner-scoped lookups. Per-language strategy:
|
||||
All languages emit unified `ExtractedHeritage` (child, parent, `EXTENDS`/`IMPLEMENTS`). MRO phase walks the heritage graph using per-language strategy:
|
||||
- **`first-wins`** — Java, C#, C++, TS, Ruby, Go
|
||||
- **`c3`** — Python (C3 linearization)
|
||||
- **`ruby-mixin`** — Ruby (mixin-aware linearization)
|
||||
- **`none`** — single-inheritance languages
|
||||
|
||||
Unified walk: `lookupMethodByOwnerWithMRO()` in `model/resolve.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Full analysis flow
|
||||
|
||||
@@ -35,7 +35,7 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g
|
||||
## Reference Documentation
|
||||
|
||||
- **This repository:** [AGENTS.md](AGENTS.md) (Cursor + monorepo notes), [ARCHITECTURE.md](ARCHITECTURE.md), [CONTRIBUTING.md](CONTRIBUTING.md), [GUARDRAILS.md](GUARDRAILS.md).
|
||||
- **Call & inheritance resolution:** See ARCHITECTURE.md § Scope-Resolution Pipeline. Shared pipeline code in `gitnexus/src/core/ingestion/` must not name languages — use `LanguageProvider` / `ScopeResolver` hooks instead (see AGENTS.md). (The legacy call-resolution DAG was removed in #942.)
|
||||
- **Call-resolution DAG:** See ARCHITECTURE.md § Call-Resolution DAG. Shared pipeline code in `gitnexus/src/core/ingestion/` must not name languages — use `LanguageProvider` hooks instead (see AGENTS.md).
|
||||
- **GitNexus:** `.claude/skills/gitnexus/`; MCP and indexed-repo rules live only in [AGENTS.md](AGENTS.md) (`gitnexus:start` … `gitnexus:end`). See **GitNexus rules** below.
|
||||
|
||||
## Changelog
|
||||
@@ -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 `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 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 warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- 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"})`.
|
||||
- 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"})`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `impact` on it.
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
+1
-8
@@ -13,8 +13,6 @@ This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformpro
|
||||
|
||||
## Development setup
|
||||
|
||||
**Prerequisites:** Node.js — `gitnexus/` requires `>=22.0.0` and `gitnexus-web/` requires `^20.19.0 || >=22.12.0` (enforced via the `engines` field in each package). Use `nvm install` to match the local version.
|
||||
|
||||
1. Clone the repository.
|
||||
2. **CLI / MCP package:** `cd gitnexus && npm install && npm run build`
|
||||
3. **Web UI (if needed):** `cd gitnexus-web && npm install`
|
||||
@@ -157,12 +155,7 @@ routes between two modes based on the triggering event:
|
||||
suffix; RC tags are excluded at trigger via a negative glob). Publishes to
|
||||
the `latest` dist-tag with a changelog-backed GitHub release. Maintainers
|
||||
are expected to tag from `main` as a convention; the workflow itself does
|
||||
not enforce branch reachability. No Docker build (RC-only). Before cutting a
|
||||
stable release, keep `gitnexus/package.json`,
|
||||
`gitnexus-claude-plugin/.claude-plugin/plugin.json`,
|
||||
`.claude-plugin/marketplace.json`, and the matching `CHANGELOG.md` entry in
|
||||
lockstep — the always-on `gitnexus` unit suite now fails if those manifest
|
||||
versions drift.
|
||||
not enforce branch reachability. No Docker build (RC-only).
|
||||
- **Release-candidate mode** — runs on every push to `main` (typically a
|
||||
merged PR) plus manual `workflow_dispatch`. Docs-only changes are skipped
|
||||
via `paths-ignore`. Publishes to the `rc` dist-tag with version
|
||||
|
||||
@@ -36,17 +36,6 @@ RUN npm ci --prefix gitnexus
|
||||
# Drop dev dependencies for a smaller runtime layer.
|
||||
RUN npm prune --omit=dev --prefix gitnexus
|
||||
|
||||
# `npm prune` removes anything not in package.json's dependency tree — which
|
||||
# includes the VENDORED tree-sitter grammars (materialized into node_modules/ by
|
||||
# postinstall, but not declared as deps) and their freshly-built native bindings.
|
||||
# The `serve` image analyzes/parses uploaded repos at runtime, so those grammars
|
||||
# must survive into the runtime layer. Re-run the grammar postinstall here in the
|
||||
# builder (which still has python3/make/g++ and the hoisted node-addon-api /
|
||||
# node-gyp-build) to re-materialize + rebuild them after the prune. This is
|
||||
# load-bearing for tree-sitter-c (a core, REQUIRED grammar now vendored, #2116):
|
||||
# as a former `dependency` it used to survive prune; vendored, it would not.
|
||||
RUN npm run postinstall --prefix gitnexus
|
||||
|
||||
# -- Runtime -----------------------------------------------------------
|
||||
# node:22-bookworm-slim
|
||||
FROM node:22-bookworm-slim@sha256:9f6d5975c7dca860947d3915877f85607946403fc55349f39b4bc3688448bb6e AS runtime
|
||||
@@ -78,28 +67,6 @@ COPY --from=builder --chown=node:node /app/gitnexus/vendor ./gitnexus/vendor
|
||||
# unreachable from $PATH.
|
||||
RUN ln -s /app/gitnexus/dist/cli/index.js /usr/local/bin/gitnexus
|
||||
|
||||
# Bake the LadybugDB FTS extension into the image so BM25 keyword search works
|
||||
# at runtime. The server runs the default `load-only` extension policy (the read
|
||||
# pool pins `{ policy: 'load-only' }`), so a runtime `LOAD EXTENSION fts` never
|
||||
# INSTALLs — the extension must already exist in the runtime user's HOME
|
||||
# extension dir, or every keyword search silently degrades (no FTS indexes are
|
||||
# written and ranking falls back to vector-only with only a `warning` field).
|
||||
# Run the installer as the `node` user with the SAME HOME the server runs under,
|
||||
# so `INSTALL fts` materializes the extension under `$HOME/.lbdb/extension` where
|
||||
# the runtime `LOAD` resolves it offline. `ENV HOME` is pinned because Docker
|
||||
# does not derive HOME from `USER`, so without it build-install and runtime-load
|
||||
# would resolve different paths. Requires network egress for the one-time
|
||||
# INSTALL; the build fails loudly if it cannot fetch the extension. The DB-size
|
||||
# default comes from GITNEXUS_LBUG_MAX_DB_SIZE (single source of truth, matches
|
||||
# the runtime) — it only sizes the throwaway scratch DB used to run INSTALL.
|
||||
# The second `--verify-only` step re-LOADs the extension in a FRESH process
|
||||
# under the same HOME, so a HOME/extension-dir mismatch fails the build here
|
||||
# rather than silently degrading keyword search to vector-only at runtime.
|
||||
ENV HOME=/home/node \
|
||||
GITNEXUS_LBUG_MAX_DB_SIZE=17179869184
|
||||
RUN su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts" \
|
||||
&& su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts --verify-only"
|
||||
|
||||
USER node
|
||||
|
||||
# The web UI defaults to http://localhost:4747 - keep that contract.
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
<a href="https://securityscorecards.dev/viewer/?uri=github.com/abhigyanpatwari/GitNexus">
|
||||
<img src="https://api.securityscorecards.dev/projects/github.com/abhigyanpatwari/GitNexus/badge" alt="OpenSSF Scorecard"/>
|
||||
</a>
|
||||
<a href="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml/badge.svg" alt="CI Workflows"/>
|
||||
</a>
|
||||
|
||||
<p><strong>Enterprise (SaaS & Self-hosted)</strong> - <a href="https://akonlabs.com">akonlabs.com</a></p>
|
||||
|
||||
@@ -117,9 +114,7 @@ That's it. This indexes the codebase, installs agent skills, registers Claude Co
|
||||
|
||||
To configure MCP for your editor, run `npx gitnexus setup` once — or set it up manually below.
|
||||
|
||||
> **Faster install (no C++ toolchain needed):** set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before `npm install -g gitnexus` to skip the vendored grammar materialize/build for `tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`, and `tree-sitter-kotlin` — those four won't be parsed, but install completes in seconds without `python3`/`make`/`g++`. Strict `=1` only — any other value falls through to the rebuild. See the `tree-sitter-kotlin` note below.
|
||||
>
|
||||
> **About `tree-sitter-kotlin`:** like Dart/Proto/Swift, Kotlin is a **vendored** grammar (under `gitnexus/vendor/tree-sitter-kotlin`). Upstream `tree-sitter-kotlin` ships **source only** (no prebuilt binaries), so GitNexus builds the Kotlin platform prebuilds itself (via the `build-tree-sitter-prebuilds` GitHub Actions workflow) and vendors them — the same uniform pipeline now used for Dart, Proto, and Swift (Swift's prebuilds were originally copied from upstream; they're now GitNexus-cross-built too). `node-gyp-build` selects the right `.node` at require time, so **no C/C++ toolchain is needed**. If no prebuild matches your platform-arch, only Kotlin (`.kt`/`.kts`) parsing is unavailable; the rest of `gitnexus` is unaffected.
|
||||
> **Faster install (no C++ toolchain needed):** set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before `npm install -g gitnexus` to skip vendored grammar materialize/build (`tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`). Dart/Proto/Swift files won't be parsed, but install completes in seconds without `python3`/`make`/`g++`. Strict `=1` only — any other value falls through to the rebuild.
|
||||
|
||||
### MCP Setup
|
||||
|
||||
@@ -225,7 +220,6 @@ args = ["-y", "gitnexus@latest", "mcp"]
|
||||
|
||||
```bash
|
||||
gitnexus setup # Configure MCP for your editors (one-time)
|
||||
gitnexus uninstall # Preview removal of GitNexus MCP/skills/hooks (add --force to apply)
|
||||
gitnexus analyze [path] # Index a repository (or update stale index)
|
||||
gitnexus analyze --repair-fts # Fast path: rebuild/verify only FTS indexes on existing index data
|
||||
gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS rebuild
|
||||
@@ -239,7 +233,7 @@ gitnexus analyze --embeddings [limit] # Enable embedding generation (slower, be
|
||||
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
|
||||
gitnexus analyze --worker-timeout 60 # Increase worker idle timeout for slow parses
|
||||
gitnexus analyze --wal-checkpoint-threshold 67108864 # 64 MiB. Control LadybugDB WAL auto-checkpoint threshold (default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
|
||||
gitnexus analyze --workers <n> # Parse worker pool size (>=1; default: cores-1, capped at 16, auto-sized to the repo). 0 is rejected — there is no sequential mode.
|
||||
gitnexus analyze --workers <n> # Parse worker pool size (default: cores-1, capped at 16; 0 = sequential)
|
||||
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
||||
gitnexus serve # Start local HTTP server (multi-repo) for web UI connection
|
||||
gitnexus list # List all indexed repositories
|
||||
@@ -262,8 +256,6 @@ gitnexus group query <name> <q> # Search execution flows across all repos in a
|
||||
gitnexus group status <name> # Check staleness of repos in a group
|
||||
```
|
||||
|
||||
> **`gitnexus uninstall`** reverses `gitnexus setup` — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified **by bundled gitnexus skill name** (e.g. `gitnexus-cli/`), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass `--force` to apply. Per-repo indexes (`gitnexus clean --all`) and the global npm package (`npm uninstall -g gitnexus`) are left for you to remove.
|
||||
|
||||
If `analyze` reports a worker parse timeout on a large or unusual repository, it keeps running and falls back safely. To give slow worker jobs more time, use `gitnexus analyze --worker-timeout 60` or set `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000`. For very large files, `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` controls the worker job byte budget.
|
||||
|
||||
#### Embeddings node limit
|
||||
@@ -319,7 +311,7 @@ Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max
|
||||
|
||||
| Variable | Default | Effect | Tune when… |
|
||||
| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size (must be ≥ 1). Equivalent to `--workers <n>`. The worker pool is the sole parse path — there is no sequential parser, so `0` is rejected with an actionable error (the pool self-heals via quarantine + respawn). | Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set `1` for a single-worker pool — not `0`. |
|
||||
| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size. `0` disables the pool (sequential fallback). Equivalent to `--workers <n>`. | Constrained containers (cgroup CPU limits), CI runners with explicit quotas, or debugging a worker-only crash via `0`. |
|
||||
| `GITNEXUS_PARSE_CHUNK_CONCURRENCY` | `2` | Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. | Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock. |
|
||||
| `GITNEXUS_VERBOSE` | unset | When `1`, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to `--verbose`. | Debugging an analyze that "completed" but seems to have missed files; tuning `--workers` / chunk concurrency against observable throughput. |
|
||||
| `GITNEXUS_PROFILE_DEFERRED` | unset | When `1`, emits `[deferred-profile]` timing/progress logs for the post-chunk deferred resolution band (imports → heritage → buildHeritageMap → legacy call resolution). Implied by `GITNEXUS_VERBOSE`. | Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise. |
|
||||
@@ -333,7 +325,7 @@ Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max
|
||||
| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD`| `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, every subsequent dispatch rejects until a fresh pool is created. | Hosts where a SIGSEGV-prone native grammar should trip the breaker sooner; CI runners that should fail loudly. |
|
||||
| `GITNEXUS_CHUNK_BYTE_BUDGET` | `2097152` (2 MB) | Chunk boundary used for cache-key composition and dispatch. Smaller = finer-grained cache hits but more dispatch overhead. | Tuning incremental-analyze cache behavior on monorepos. |
|
||||
| `GITNEXUS_NO_GITIGNORE` | unset | When set, skips `.gitignore` parsing. `.gitnexusignore` is still honored. | Indexing a repo whose `.gitignore` excludes files you actually want indexed (e.g., generated code committed for cross-repo lookup). |
|
||||
| `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` | unset | When `=1` strictly, skips the vendored grammar materialize for `tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`, and `tree-sitter-kotlin` at install time (and the Dart/Proto source builds). Those four won't be parsed; the install still succeeds. | Installing on a host without a C++ toolchain or where the vendored prebuilds don't match; willing to skip Dart/Proto/Swift/Kotlin parsing. |
|
||||
| `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` | unset | When `=1` strictly, skips vendored grammar materialize/build for `tree-sitter-dart`, `tree-sitter-proto`, and `tree-sitter-swift` at install time. | Installing on a host without a C++ toolchain or where Swift prebuilds don't match; you're willing to skip Dart/Proto/Swift parsing. |
|
||||
|
||||
#### Publishing to understand-quickly (opt-in)
|
||||
|
||||
@@ -347,7 +339,7 @@ It is opt-in and a no-op without `UNDERSTAND_QUICKLY_TOKEN` — a fine-grained G
|
||||
|
||||
| Tool | What It Does | `repo` Param |
|
||||
| ----------------- | ---------------------------------------------------------------- | ------------ |
|
||||
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) | — |
|
||||
| `list_repos` | Discover all indexed repositories | — |
|
||||
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) | Optional |
|
||||
| `context` | 360-degree symbol view — categorized refs, process participation | Optional |
|
||||
| `impact` | Blast radius analysis with depth grouping and confidence | Optional |
|
||||
|
||||
+42
-32
@@ -4,11 +4,11 @@ How we structure tests and which commands to run locally and in CI.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Path | Runner | Notes |
|
||||
| -------------- | --------------- | ---------- | -------------------------- |
|
||||
| CLI + MCP core | `gitnexus/` | Vitest | Primary test surface in CI |
|
||||
| Web UI | `gitnexus-web/` | Vitest | Unit/component tests |
|
||||
| Web UI E2E | `gitnexus-web/` | Playwright | Run when changing UI flows |
|
||||
| Package | Path | Runner | Notes |
|
||||
| -------------- | -------------- | -------- | ------------------------------ |
|
||||
| CLI + MCP core | `gitnexus/` | Vitest | Primary test surface in CI |
|
||||
| Web UI | `gitnexus-web/`| Vitest | Unit/component tests |
|
||||
| Web UI E2E | `gitnexus-web/`| Playwright | Run when changing UI flows |
|
||||
|
||||
## Test lanes
|
||||
|
||||
@@ -16,25 +16,25 @@ How we structure tests and which commands to run locally and in CI.
|
||||
|
||||
From `gitnexus/`:
|
||||
|
||||
| Command | What it runs | When to use |
|
||||
| ----------------------------- | -------------------------------------------------- | ------------------------------------- |
|
||||
| `npm test` | Full suite (all 3 vitest projects) | Before opening a PR |
|
||||
| `npm run test:unit` | Unit tests only (`test/unit/`) | Tight development loop |
|
||||
| `npm run test:integration` | Integration tests (`test/integration/`) | After changing pipelines, DB, workers |
|
||||
| `npm run test:coverage` | Full suite + v8 coverage with thresholds | Checking coverage impact |
|
||||
| `npm run test:parity` | Scope-resolution parity for all migrated languages | After changing resolver or scope code |
|
||||
| `npm run test:cross-platform` | Platform-sensitive subset only | Debugging a Windows/macOS issue |
|
||||
| `npm run test:watch` | Vitest in watch mode | Active development |
|
||||
| Command | What it runs | When to use |
|
||||
| ------------------------ | ---------------------------------------------------- | ------------------------------- |
|
||||
| `npm test` | Full suite (all 3 vitest projects) | Before opening a PR |
|
||||
| `npm run test:unit` | Unit tests only (`test/unit/`) | Tight development loop |
|
||||
| `npm run test:integration` | Integration tests (`test/integration/`) | After changing pipelines, DB, workers |
|
||||
| `npm run test:coverage` | Full suite + v8 coverage with thresholds | Checking coverage impact |
|
||||
| `npm run test:parity` | Scope-resolution parity for all migrated languages | After changing resolver or scope code |
|
||||
| `npm run test:cross-platform` | Platform-sensitive subset only | Debugging a Windows/macOS issue |
|
||||
| `npm run test:watch` | Vitest in watch mode | Active development |
|
||||
|
||||
### `gitnexus-web/` commands
|
||||
|
||||
From `gitnexus-web/`:
|
||||
|
||||
| Command | What it runs | When to use |
|
||||
| ----------------------- | ----------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm test` | Unit/component tests (vitest) | After changing web code |
|
||||
| `npm run test:coverage` | Unit tests + coverage | Checking coverage impact |
|
||||
| `npm run test:e2e` | Playwright browser tests | After changing UI flows (requires `gitnexus serve` + `npm run dev`) |
|
||||
| Command | What it runs | When to use |
|
||||
| ---------------------- | --------------------------------- | ------------------------------ |
|
||||
| `npm test` | Unit/component tests (vitest) | After changing web code |
|
||||
| `npm run test:coverage`| Unit tests + coverage | Checking coverage impact |
|
||||
| `npm run test:e2e` | Playwright browser tests | After changing UI flows (requires `gitnexus serve` + `npm run dev`) |
|
||||
|
||||
### Before opening a PR
|
||||
|
||||
@@ -59,11 +59,11 @@ Skip with `git commit --no-verify` (use sparingly).
|
||||
|
||||
`gitnexus/vitest.config.ts` defines three projects for safety isolation:
|
||||
|
||||
| Project | Files | Parallelism | Purpose |
|
||||
| --------- | -------------------------------------------------- | ----------- | --------------------------------------------------- |
|
||||
| `lbug-db` | Native LadybugDB integration tests (explicit list) | Sequential | Prevents file-lock conflicts from native mmap addon |
|
||||
| `cli-e2e` | `skills-e2e.test.ts` | Sequential | CLI process spawning requires serial execution |
|
||||
| `default` | Everything else | Parallel | Fast execution for pure logic and parser tests |
|
||||
| Project | Files | Parallelism | Purpose |
|
||||
| ---------- | ----------------------------- | ----------- | ---------------------------------------------- |
|
||||
| `lbug-db` | Native LadybugDB integration tests (explicit list) | Sequential | Prevents file-lock conflicts from native mmap addon |
|
||||
| `cli-e2e` | `skills-e2e.test.ts` | Sequential | CLI process spawning requires serial execution |
|
||||
| `default` | Everything else | Parallel | Fast execution for pure logic and parser tests |
|
||||
|
||||
When adding a new test that uses native LadybugDB (`@ladybugdb/core`), add it to the `lbug-db` project's explicit include list and the `default` project's exclude list.
|
||||
|
||||
@@ -74,11 +74,21 @@ When adding a new test that uses native LadybugDB (`@ladybugdb/core`), add it to
|
||||
- **Resolver / parity** — Language-specific call-resolution tests in `test/integration/resolvers/`.
|
||||
- **E2E (web)** — Critical user paths only; prefer `data-testid` attributes for stable selectors. Tests run against real backend (`gitnexus serve`) and Vite dev server.
|
||||
|
||||
## Scope-resolution tests
|
||||
## Scope-resolution parity
|
||||
|
||||
Every language resolves calls and inheritance through the scope-resolution pipeline — the legacy call-resolution DAG and the per-language `REGISTRY_PRIMARY_<LANG>` flag were removed in RING4-1 (#942). Each language's resolver test lives at `test/integration/resolvers/<slug>.test.ts` and runs once, on the single scope-resolution path, as part of the normal `tests` job (`vitest test/**/*.test.ts`).
|
||||
Migrated languages (listed in `MIGRATED_LANGUAGES` in `src/core/ingestion/registry-primary-flag.ts`) are tested in both legacy and registry-primary modes on every PR.
|
||||
|
||||
Adding a language: register its `ScopeResolver` in `scope-resolution/pipeline/registry.ts` (`SCOPE_RESOLVERS`) and add the resolver test file — no workflow or config edit needed.
|
||||
For each migrated language, CI runs the resolver test file twice:
|
||||
1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path
|
||||
2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path
|
||||
|
||||
Both must pass. Known legacy gaps are listed in `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES` in `test/integration/resolvers/helpers.ts` and are automatically skipped in legacy mode.
|
||||
|
||||
Adding a language to `MIGRATED_LANGUAGES` automatically enrolls it in parity — no workflow or config edit needed. The test file must exist at `test/integration/resolvers/<slug>.test.ts`.
|
||||
|
||||
Run parity locally: `cd gitnexus && npm run test:parity`
|
||||
|
||||
Run for a single language: `cd gitnexus && npx tsx scripts/run-parity.ts --language python`
|
||||
|
||||
## Cross-platform testing
|
||||
|
||||
@@ -110,12 +120,12 @@ To check the cross-platform list is up to date, run `npm run test:cross-platform
|
||||
|
||||
GitHub Actions (`.github/workflows/ci.yml`) orchestrate:
|
||||
|
||||
| Workflow | Jobs | Purpose |
|
||||
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
|
||||
| Workflow | Jobs | Purpose |
|
||||
| --------------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
|
||||
| `ci-tests.yml` | ubuntu/coverage, cross-platform (Win/Mac), packaged-install-smoke | Full suite + coverage on Ubuntu; platform-sensitive subset on Win/Mac |
|
||||
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
|
||||
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
|
||||
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
|
||||
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
|
||||
|
||||
The `CI Gate` job in `ci.yml` is the single required check for branch protection. It requires quality, tests, e2e, and scope-parity to all pass.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"description": "Code intelligence powered by a knowledge graph. Provides execution flow tracing, blast radius analysis, and augmented search across your codebase.",
|
||||
"version": "1.6.7",
|
||||
"version": "1.3.6",
|
||||
"author": {
|
||||
"name": "GitNexus"
|
||||
},
|
||||
|
||||
@@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. context({name: "<suspect>"}) → See callers/callees/processes
|
||||
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
|
||||
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
|
||||
4. cypher({query: "MATCH path..."}) → Custom traces if needed
|
||||
4. gitnexus_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)
|
||||
- [ ] query for error text or related code
|
||||
- [ ] gitnexus_query for error text or related code
|
||||
- [ ] Identify the suspect function from returned processes
|
||||
- [ ] context to see callers and callees
|
||||
- [ ] gitnexus_context to see callers and callees
|
||||
- [ ] Trace execution flow via process resource if applicable
|
||||
- [ ] cypher for custom call chain traces if needed
|
||||
- [ ] gitnexus_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 | `query` for error text → `context` on throw sites |
|
||||
| Error message | `gitnexus_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
|
||||
|
||||
**query** — find code related to error:
|
||||
**gitnexus_query** — find code related to error:
|
||||
|
||||
```
|
||||
query({query: "payment validation error"})
|
||||
gitnexus_query({query: "payment validation error"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError, PaymentException
|
||||
```
|
||||
|
||||
**context** — full context for a suspect:
|
||||
**gitnexus_context** — full context for a suspect:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
→ Processes: CheckoutFlow (step 3/7)
|
||||
```
|
||||
|
||||
**cypher** — custom call chain traces:
|
||||
**gitnexus_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. query({query: "payment error handling"})
|
||||
1. gitnexus_query({query: "payment error handling"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError
|
||||
|
||||
2. context({name: "validatePayment"})
|
||||
2. gitnexus_context({name: "validatePayment"})
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
|
||||
@@ -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. query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
||||
```
|
||||
|
||||
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
|
||||
|
||||
```
|
||||
- [ ] READ gitnexus://repo/{name}/context
|
||||
- [ ] query for the concept you want to understand
|
||||
- [ ] gitnexus_query for the concept you want to understand
|
||||
- [ ] Review returned processes (execution flows)
|
||||
- [ ] context on key symbols for callers/callees
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**query** — find execution flows related to a concept:
|
||||
**gitnexus_query** — find execution flows related to a concept:
|
||||
|
||||
```
|
||||
query({query: "payment processing"})
|
||||
gitnexus_query({query: "payment processing"})
|
||||
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Symbols grouped by flow with file locations
|
||||
```
|
||||
|
||||
**context** — 360-degree view of a symbol:
|
||||
**gitnexus_context** — 360-degree view of a symbol:
|
||||
|
||||
```
|
||||
context({name: "validateUser"})
|
||||
gitnexus_context({name: "validateUser"})
|
||||
→ Incoming calls: loginHandler, apiMiddleware
|
||||
→ Outgoing calls: checkToken, getUserById
|
||||
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
|
||||
@@ -68,10 +68,10 @@ context({name: "validateUser"})
|
||||
|
||||
```
|
||||
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
|
||||
2. query({query: "payment processing"})
|
||||
2. gitnexus_query({query: "payment processing"})
|
||||
→ CheckoutFlow: processPayment → validateCard → chargeStripe
|
||||
→ RefundFlow: initiateRefund → calculateRefund → processRefund
|
||||
3. context({name: "processPayment"})
|
||||
3. gitnexus_context({name: "processPayment"})
|
||||
→ Incoming: checkoutHandler, webhookHandler
|
||||
→ Outgoing: validateCard, chargeStripe, saveTransaction
|
||||
4. Read src/payments/processor.ts for implementation details
|
||||
|
||||
@@ -38,38 +38,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
||||
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
||||
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
||||
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
||||
| `list_repos` | Discover indexed repos (paginated — `limit`/`offset`) |
|
||||
|
||||
### Paginating `list_repos`
|
||||
|
||||
`list_repos` is paginated so a large registry is not truncated by MCP/LLM token limits. It takes optional `limit` (default **50**, max **200**) and `offset`, and returns:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"repositories": [
|
||||
{ "name": "...", "path": "...", "indexedAt": "...", "lastCommit": "...", "stats": { } }
|
||||
],
|
||||
"pagination": {
|
||||
"total": 437,
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
"returned": 50,
|
||||
"hasMore": true,
|
||||
"nextOffset": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To enumerate **every** repository, keep calling with `offset` set to `pagination.nextOffset` until `hasMore` is `false`:
|
||||
|
||||
```text
|
||||
list_repos {} → repos 1–50, nextOffset 50, hasMore true
|
||||
list_repos { offset: 50 } → repos 51–100, nextOffset 100, hasMore true
|
||||
…
|
||||
list_repos { offset: 400 } → repos 401–437, hasMore false (done)
|
||||
```
|
||||
|
||||
Notes: `offset` ≥ `total` returns an empty page (with `total` still reported). Out-of-range or malformed `limit`/`offset` (non-integer, `limit` outside `[1, 200]`, `offset < 0`) are rejected with a clear error — `limit` above the max is rejected, not silently capped. The order is deterministic (lower-cased name, then path), so paging never skips or duplicates an entry while the registry is unchanged.
|
||||
| `list_repos` | Discover indexed repos |
|
||||
|
||||
## Resources Reference
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
3. detect_changes() → Map current git changes to affected flows
|
||||
3. gitnexus_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
|
||||
|
||||
```
|
||||
- [ ] impact({target, direction: "upstream"}) to find dependents
|
||||
- [ ] gitnexus_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
|
||||
- [ ] detect_changes() for pre-commit check
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**impact** — the primary tool for symbol blast radius:
|
||||
**gitnexus_impact** — the primary tool for symbol blast radius:
|
||||
|
||||
```
|
||||
impact({
|
||||
gitnexus_impact({
|
||||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
@@ -73,10 +73,10 @@ impact({
|
||||
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**detect_changes** — git-diff based impact analysis:
|
||||
**gitnexus_detect_changes** — git-diff based impact analysis:
|
||||
|
||||
```
|
||||
detect_changes({scope: "staged"})
|
||||
gitnexus_detect_changes({scope: "staged"})
|
||||
|
||||
→ Changed: 5 symbols in 3 files
|
||||
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
|
||||
@@ -86,7 +86,7 @@ detect_changes({scope: "staged"})
|
||||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
|
||||
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
1. gh pr diff <number> → Get the raw diff
|
||||
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
3. For each changed symbol:
|
||||
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. context({name: "<key symbol>"}) → Understand callers/callees
|
||||
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
|
||||
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
6. Summarize findings with risk assessment
|
||||
```
|
||||
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
|
||||
- [ ] detect_changes to map changes to affected execution flows
|
||||
- [ ] impact on each non-trivial changed symbol
|
||||
- [ ] gitnexus_detect_changes to map changes to affected execution flows
|
||||
- [ ] gitnexus_impact on each non-trivial changed symbol
|
||||
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
|
||||
- [ ] context on key changed symbols to understand full picture
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**detect_changes** — map PR diff to affected execution flows:
|
||||
**gitnexus_detect_changes** — map PR diff to affected execution flows:
|
||||
|
||||
```
|
||||
detect_changes({scope: "compare", base_ref: "main"})
|
||||
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
|
||||
→ Changed: 8 symbols in 4 files
|
||||
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**impact** — blast radius per changed symbol:
|
||||
**gitnexus_impact** — blast radius per changed symbol:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream"})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
|
||||
@@ -86,20 +86,20 @@ impact({target: "validatePayment", direction: "upstream"})
|
||||
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**impact with tests** — check test coverage:
|
||||
**gitnexus_impact with tests** — check test coverage:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
|
||||
→ Tests that cover this symbol:
|
||||
- validatePayment.test.ts [direct]
|
||||
- checkout.integration.test.ts [via processCheckout]
|
||||
```
|
||||
|
||||
**context** — understand a changed symbol's role:
|
||||
**gitnexus_context** — understand a changed symbol's role:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates
|
||||
@@ -112,20 +112,20 @@ context({name: "validatePayment"})
|
||||
1. gh pr diff 42 > /tmp/pr42.diff
|
||||
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
|
||||
|
||||
2. detect_changes({scope: "compare", base_ref: "main"})
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
→ Changed symbols: validatePayment, PaymentInput, formatAmount
|
||||
→ Affected processes: CheckoutFlow, RefundFlow
|
||||
→ Risk: MEDIUM
|
||||
|
||||
3. impact({target: "validatePayment", direction: "upstream"})
|
||||
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
→ d=1: processCheckout, webhookHandler (WILL BREAK)
|
||||
→ webhookHandler is NOT in the PR diff — potential breakage!
|
||||
|
||||
4. impact({target: "PaymentInput", direction: "upstream"})
|
||||
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
|
||||
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
|
||||
→ createPayment uses the old PaymentInput shape — breaking change!
|
||||
|
||||
5. context({name: "formatAmount"})
|
||||
5. gitnexus_context({name: "formatAmount"})
|
||||
→ Called by 12 functions — but change is backwards-compatible (added optional param)
|
||||
|
||||
6. Review summary:
|
||||
|
||||
@@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
||||
## Workflow
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
```
|
||||
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
|
||||
- [ ] gitnexus_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: rename({..., dry_run: false}) — apply edits
|
||||
- [ ] detect_changes() — verify only expected files changed
|
||||
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
|
||||
- [ ] gitnexus_detect_changes() — verify only expected files changed
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Extract Module
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] Define new module interface
|
||||
- [ ] Extract code, update imports
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Split Function/Service
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — understand all callees
|
||||
- [ ] gitnexus_context({name: target}) — understand all callees
|
||||
- [ ] Group callees by responsibility
|
||||
- [ ] impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] Create new functions/services
|
||||
- [ ] Update callers
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
**rename** — automated multi-file rename:
|
||||
**gitnexus_rename** — automated multi-file rename:
|
||||
|
||||
```
|
||||
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
gitnexus_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}]}]
|
||||
```
|
||||
|
||||
**impact** — map all dependents first:
|
||||
**gitnexus_impact** — map all dependents first:
|
||||
|
||||
```
|
||||
impact({target: "validateUser", direction: "upstream"})
|
||||
gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware, testUtils
|
||||
→ Affected Processes: LoginFlow, TokenRefresh
|
||||
```
|
||||
|
||||
**detect_changes** — verify your changes after refactoring:
|
||||
**gitnexus_detect_changes** — verify your changes after refactoring:
|
||||
|
||||
```
|
||||
detect_changes({scope: "all"})
|
||||
gitnexus_detect_changes({scope: "all"})
|
||||
→ Changed: 8 files, 12 symbols
|
||||
→ Affected processes: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**cypher** — custom reference queries:
|
||||
**gitnexus_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 rename for automated updates |
|
||||
| Many callers (>5) | Use gitnexus_rename for automated updates |
|
||||
| Cross-area refs | Use detect_changes after to verify scope |
|
||||
| String/dynamic refs | query to find them |
|
||||
| String/dynamic refs | gitnexus_query to find them |
|
||||
| External/public API | Version and deprecate properly |
|
||||
|
||||
## Example: Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
1. gitnexus_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. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
→ Applied 12 edits across 8 files
|
||||
|
||||
4. detect_changes({scope: "all"})
|
||||
4. gitnexus_detect_changes({scope: "all"})
|
||||
→ Affected: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM — run tests for these flows
|
||||
```
|
||||
|
||||
@@ -15,10 +15,10 @@ description: Trace bugs through call chains using knowledge graph
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. context({name: "<suspect>"}) → See callers/callees/processes
|
||||
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
|
||||
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
|
||||
4. cypher({query: "MATCH path..."}) → Custom traces if needed
|
||||
4. gitnexus_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)
|
||||
- [ ] query for error text or related code
|
||||
- [ ] gitnexus_query for error text or related code
|
||||
- [ ] Identify the suspect function from returned processes
|
||||
- [ ] context to see callers and callees
|
||||
- [ ] gitnexus_context to see callers and callees
|
||||
- [ ] Trace execution flow via process resource if applicable
|
||||
- [ ] cypher for custom call chain traces if needed
|
||||
- [ ] gitnexus_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 | `query` for error text → `context` on throw sites |
|
||||
| Error message | `gitnexus_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
|
||||
|
||||
**query** — find code related to error:
|
||||
**gitnexus_query** — find code related to error:
|
||||
```
|
||||
query({query: "payment validation error"})
|
||||
gitnexus_query({query: "payment validation error"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError, PaymentException
|
||||
```
|
||||
|
||||
**context** — full context for a suspect:
|
||||
**gitnexus_context** — full context for a suspect:
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
→ Processes: CheckoutFlow (step 3/7)
|
||||
```
|
||||
|
||||
**cypher** — custom call chain traces:
|
||||
**gitnexus_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. query({query: "payment error handling"})
|
||||
1. gitnexus_query({query: "payment error handling"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError
|
||||
|
||||
2. context({name: "validatePayment"})
|
||||
2. gitnexus_context({name: "validatePayment"})
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
|
||||
@@ -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. query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
||||
```
|
||||
|
||||
@@ -28,9 +28,9 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
|
||||
|
||||
```
|
||||
- [ ] READ gitnexus://repo/{name}/context
|
||||
- [ ] query for the concept you want to understand
|
||||
- [ ] gitnexus_query for the concept you want to understand
|
||||
- [ ] Review returned processes (execution flows)
|
||||
- [ ] context on key symbols for callers/callees
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**query** — find execution flows related to a concept:
|
||||
**gitnexus_query** — find execution flows related to a concept:
|
||||
```
|
||||
query({query: "payment processing"})
|
||||
gitnexus_query({query: "payment processing"})
|
||||
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Symbols grouped by flow with file locations
|
||||
```
|
||||
|
||||
**context** — 360-degree view of a symbol:
|
||||
**gitnexus_context** — 360-degree view of a symbol:
|
||||
```
|
||||
context({name: "validateUser"})
|
||||
gitnexus_context({name: "validateUser"})
|
||||
→ Incoming calls: loginHandler, apiMiddleware
|
||||
→ Outgoing calls: checkToken, getUserById
|
||||
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
|
||||
@@ -65,10 +65,10 @@ context({name: "validateUser"})
|
||||
|
||||
```
|
||||
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
|
||||
2. query({query: "payment processing"})
|
||||
2. gitnexus_query({query: "payment processing"})
|
||||
→ CheckoutFlow: processPayment → validateCard → chargeStripe
|
||||
→ RefundFlow: initiateRefund → calculateRefund → processRefund
|
||||
3. context({name: "processPayment"})
|
||||
3. gitnexus_context({name: "processPayment"})
|
||||
→ Incoming: checkoutHandler, webhookHandler
|
||||
→ Outgoing: validateCard, chargeStripe, saveTransaction
|
||||
4. Read src/payments/processor.ts for implementation details
|
||||
|
||||
@@ -16,9 +16,9 @@ description: Analyze blast radius before making code changes
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
3. detect_changes() → Map current git changes to affected flows
|
||||
3. gitnexus_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
|
||||
|
||||
```
|
||||
- [ ] impact({target, direction: "upstream"}) to find dependents
|
||||
- [ ] gitnexus_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
|
||||
- [ ] detect_changes() for pre-commit check
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**impact** — the primary tool for symbol blast radius:
|
||||
**gitnexus_impact** — the primary tool for symbol blast radius:
|
||||
```
|
||||
impact({
|
||||
gitnexus_impact({
|
||||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
@@ -71,9 +71,9 @@ impact({
|
||||
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**detect_changes** — git-diff based impact analysis:
|
||||
**gitnexus_detect_changes** — git-diff based impact analysis:
|
||||
```
|
||||
detect_changes({scope: "staged"})
|
||||
gitnexus_detect_changes({scope: "staged"})
|
||||
|
||||
→ Changed: 5 symbols in 3 files
|
||||
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
|
||||
@@ -83,7 +83,7 @@ detect_changes({scope: "staged"})
|
||||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
|
||||
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
1. gh pr diff <number> → Get the raw diff
|
||||
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
3. For each changed symbol:
|
||||
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. context({name: "<key symbol>"}) → Understand callers/callees
|
||||
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
|
||||
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
6. Summarize findings with risk assessment
|
||||
```
|
||||
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
|
||||
- [ ] detect_changes to map changes to affected execution flows
|
||||
- [ ] impact on each non-trivial changed symbol
|
||||
- [ ] gitnexus_detect_changes to map changes to affected execution flows
|
||||
- [ ] gitnexus_impact on each non-trivial changed symbol
|
||||
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
|
||||
- [ ] context on key changed symbols to understand full picture
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**detect_changes** — map PR diff to affected execution flows:
|
||||
**gitnexus_detect_changes** — map PR diff to affected execution flows:
|
||||
|
||||
```
|
||||
detect_changes({scope: "compare", base_ref: "main"})
|
||||
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
|
||||
→ Changed: 8 symbols in 4 files
|
||||
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**impact** — blast radius per changed symbol:
|
||||
**gitnexus_impact** — blast radius per changed symbol:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream"})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
|
||||
@@ -86,20 +86,20 @@ impact({target: "validatePayment", direction: "upstream"})
|
||||
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**impact with tests** — check test coverage:
|
||||
**gitnexus_impact with tests** — check test coverage:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
|
||||
→ Tests that cover this symbol:
|
||||
- validatePayment.test.ts [direct]
|
||||
- checkout.integration.test.ts [via processCheckout]
|
||||
```
|
||||
|
||||
**context** — understand a changed symbol's role:
|
||||
**gitnexus_context** — understand a changed symbol's role:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates
|
||||
@@ -112,20 +112,20 @@ context({name: "validatePayment"})
|
||||
1. gh pr diff 42 > /tmp/pr42.diff
|
||||
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
|
||||
|
||||
2. detect_changes({scope: "compare", base_ref: "main"})
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
→ Changed symbols: validatePayment, PaymentInput, formatAmount
|
||||
→ Affected processes: CheckoutFlow, RefundFlow
|
||||
→ Risk: MEDIUM
|
||||
|
||||
3. impact({target: "validatePayment", direction: "upstream"})
|
||||
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
→ d=1: processCheckout, webhookHandler (WILL BREAK)
|
||||
→ webhookHandler is NOT in the PR diff — potential breakage!
|
||||
|
||||
4. impact({target: "PaymentInput", direction: "upstream"})
|
||||
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
|
||||
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
|
||||
→ createPayment uses the old PaymentInput shape — breaking change!
|
||||
|
||||
5. context({name: "formatAmount"})
|
||||
5. gitnexus_context({name: "formatAmount"})
|
||||
→ Called by 12 functions — but change is backwards-compatible (added optional param)
|
||||
|
||||
6. Review summary:
|
||||
|
||||
@@ -15,9 +15,9 @@ description: Plan safe refactors using blast radius and dependency mapping
|
||||
## Workflow
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
4. Plan update order: interfaces → implementations → callers → tests
|
||||
```
|
||||
|
||||
@@ -27,60 +27,60 @@ description: Plan safe refactors using blast radius and dependency mapping
|
||||
|
||||
### Rename Symbol
|
||||
```
|
||||
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
|
||||
- [ ] gitnexus_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: rename({..., dry_run: false}) — apply edits
|
||||
- [ ] detect_changes() — verify only expected files changed
|
||||
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
|
||||
- [ ] gitnexus_detect_changes() — verify only expected files changed
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Extract Module
|
||||
```
|
||||
- [ ] context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] Define new module interface
|
||||
- [ ] Extract code, update imports
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Split Function/Service
|
||||
```
|
||||
- [ ] context({name: target}) — understand all callees
|
||||
- [ ] gitnexus_context({name: target}) — understand all callees
|
||||
- [ ] Group callees by responsibility
|
||||
- [ ] impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] Create new functions/services
|
||||
- [ ] Update callers
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
**rename** — automated multi-file rename:
|
||||
**gitnexus_rename** — automated multi-file rename:
|
||||
```
|
||||
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
gitnexus_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}]}]
|
||||
```
|
||||
|
||||
**impact** — map all dependents first:
|
||||
**gitnexus_impact** — map all dependents first:
|
||||
```
|
||||
impact({target: "validateUser", direction: "upstream"})
|
||||
gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware, testUtils
|
||||
→ Affected Processes: LoginFlow, TokenRefresh
|
||||
```
|
||||
|
||||
**detect_changes** — verify your changes after refactoring:
|
||||
**gitnexus_detect_changes** — verify your changes after refactoring:
|
||||
```
|
||||
detect_changes({scope: "all"})
|
||||
gitnexus_detect_changes({scope: "all"})
|
||||
→ Changed: 8 files, 12 symbols
|
||||
→ Affected processes: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**cypher** — custom reference queries:
|
||||
**gitnexus_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 rename for automated updates |
|
||||
| Many callers (>5) | Use gitnexus_rename for automated updates |
|
||||
| Cross-area refs | Use detect_changes after to verify scope |
|
||||
| String/dynamic refs | query to find them |
|
||||
| String/dynamic refs | gitnexus_query to find them |
|
||||
| External/public API | Version and deprecate properly |
|
||||
|
||||
## Example: Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
1. gitnexus_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. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
→ Applied 12 edits across 8 files
|
||||
|
||||
4. detect_changes({scope: "all"})
|
||||
4. gitnexus_detect_changes({scope: "all"})
|
||||
→ Affected: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM — run tests for these flows
|
||||
```
|
||||
|
||||
@@ -44,10 +44,7 @@ export type NodeLabel =
|
||||
| 'Template'
|
||||
| 'Section'
|
||||
| 'Route'
|
||||
| 'Tool'
|
||||
// Taint/PDG substrate (issue #2080). Intra-procedural control-flow node.
|
||||
// Emitted by no phase yet — M1 (#2081) populates these behind an opt-in.
|
||||
| 'BasicBlock';
|
||||
| 'Tool';
|
||||
|
||||
export type NodeProperties = {
|
||||
name: string;
|
||||
@@ -92,8 +89,6 @@ export type NodeProperties = {
|
||||
responseKeys?: string[];
|
||||
errorKeys?: string[];
|
||||
middleware?: string[];
|
||||
// BasicBlock (taint/PDG substrate, issue #2080) — reuses filePath/startLine/endLine.
|
||||
text?: string;
|
||||
// Extensible
|
||||
[key: string]: unknown;
|
||||
};
|
||||
@@ -136,28 +131,7 @@ export type RelationshipType =
|
||||
* `reason` encodes the event name: `vue-emit: <eventName>`.
|
||||
* Complements `BINDS_EVENT_HANDLER`; a Cypher query joining on the
|
||||
* component File node reveals all (emitter, handler) pairs. */
|
||||
| 'EMITS_EVENT'
|
||||
// ── Taint/PDG substrate (issue #2080) ────────────────────────────────────
|
||||
// Reserved edge types for the taint-first PDG substrate. No phase emits any
|
||||
// of these yet; they are populated behind an opt-in by later milestones
|
||||
// (CFG → M1 #2081, REACHING_DEF → M2 #2082, TAINTED/SANITIZES/TAINT_PATH →
|
||||
// M3/M4 #2083/#2084). Adding them here keeps the shared schema stable so
|
||||
// downstream work does not re-ripple the exhaustiveness sites.
|
||||
/** Control-flow edge between two BasicBlock nodes (intra-procedural CFG). */
|
||||
| 'CFG'
|
||||
/** Data-dependence edge: a definition of `variable` reaches a use of it.
|
||||
* The `variable` name is stored in the relation's existing `reason` column
|
||||
* (M0/S1 verdict: LadybugDB has no secondary index on relationship
|
||||
* properties, so a dedicated indexed column would not speed the
|
||||
* variable-filtered path query). */
|
||||
| 'REACHING_DEF'
|
||||
/** A tainted value flows from source toward sink. */
|
||||
| 'TAINTED'
|
||||
/** A sanitizer clears taint along a flow. */
|
||||
| 'SANITIZES'
|
||||
/** Materialized source→sink taint path. Working name — final name/representation
|
||||
* is confirmed when M3/M4 emits it; no persisted edge exists before then. */
|
||||
| 'TAINT_PATH';
|
||||
| 'EMITS_EVENT';
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
|
||||
@@ -183,3 +183,13 @@ export {
|
||||
stripGitSuffix,
|
||||
} from './integrations/understand-quickly.js';
|
||||
export type { UqDispatchPayload } from './integrations/understand-quickly.js';
|
||||
|
||||
// Shadow-mode diff + aggregation (RFC §6.3; Ring 2 SHARED #918)
|
||||
export { diffResolutions } from './scope-resolution/shadow/diff.js';
|
||||
export type {
|
||||
ShadowAgreement,
|
||||
ShadowCallsite,
|
||||
ShadowDiff,
|
||||
} from './scope-resolution/shadow/diff.js';
|
||||
export { aggregateDiffs } from './scope-resolution/shadow/aggregate.js';
|
||||
export type { LanguageParityRow, ShadowParityReport } from './scope-resolution/shadow/aggregate.js';
|
||||
|
||||
@@ -40,8 +40,6 @@ export const NODE_TABLES = [
|
||||
'Module',
|
||||
'Route',
|
||||
'Tool',
|
||||
// Taint/PDG substrate (issue #2080) — inert until M1 (#2081) emits blocks.
|
||||
'BasicBlock',
|
||||
] as const;
|
||||
|
||||
export type NodeTableName = (typeof NODE_TABLES)[number];
|
||||
@@ -69,14 +67,6 @@ export const REL_TYPES = [
|
||||
'ENTRY_POINT_OF',
|
||||
'WRAPS',
|
||||
'QUERIES',
|
||||
// Taint/PDG substrate (issue #2080) — reserved edge types, emitted by no
|
||||
// phase yet (CFG → M1, REACHING_DEF → M2, TAINTED/SANITIZES/TAINT_PATH →
|
||||
// M3/M4). REACHING_DEF's variable name rides the relation's `reason` column.
|
||||
'CFG',
|
||||
'REACHING_DEF',
|
||||
'TAINTED',
|
||||
'SANITIZES',
|
||||
'TAINT_PATH',
|
||||
] as const;
|
||||
|
||||
export type RelType = (typeof REL_TYPES)[number];
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #913.
|
||||
*
|
||||
* Consumed by: #915 (SCC finalize link pass).
|
||||
* Consumed by: #915 (SCC finalize link pass), #923 (shadow harness when
|
||||
* resolving callsite file → enclosing module).
|
||||
*/
|
||||
|
||||
import type { ScopeId } from './types.js';
|
||||
|
||||
@@ -74,28 +74,4 @@ export interface ParsedFile {
|
||||
*/
|
||||
readonly localDefs: readonly SymbolDefinition[];
|
||||
readonly referenceSites: readonly ReferenceSite[];
|
||||
/**
|
||||
* Opaque, language-private serialization of capture-time side-channel
|
||||
* state that a provider's `emitScopeCaptures` populates into module-level
|
||||
* maps as a SIDE EFFECT (not onto the scopes/defs of this `ParsedFile`).
|
||||
*
|
||||
* Such state is computed inside the parse worker (where `emitScopeCaptures`
|
||||
* runs) and would otherwise be lost across the worker→main MessageChannel
|
||||
* and the disk store, because scope-resolution reuses the serialized
|
||||
* `ParsedFile` and SKIPS re-extraction on the main thread (#1983 — the
|
||||
* whole point is to avoid a main-thread tree-sitter re-parse). Carrying the
|
||||
* data here lets the main thread repopulate those maps WITHOUT re-parsing.
|
||||
*
|
||||
* Shared / ingestion code treats this as opaque (`unknown`) per AGENTS.md
|
||||
* (no language names in shared code). The producing language fills it via
|
||||
* the `LanguageProvider.collectCaptureSideChannel` hook (worker side) and
|
||||
* consumes it via the `ScopeResolver.applyCaptureSideChannel` hook
|
||||
* (main-thread resolution side). It MUST be plain JSON-serializable data
|
||||
* (objects / arrays / primitives) so it round-trips through the disk-backed
|
||||
* `parsedfile-store` (JSON.stringify + interning reviver).
|
||||
*
|
||||
* Optional: providers whose `emitScopeCaptures` is pure (no module-level
|
||||
* side effects — the contract default) leave this undefined.
|
||||
*/
|
||||
readonly captureSideChannel?: unknown;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ export interface RawSignals {
|
||||
*
|
||||
* Emission order mirrors the `EvidenceWeights` layout: where-found →
|
||||
* type-binding → corroborators → arity → degraded. Stable order makes
|
||||
* the per-signal contributions easy to reason about in tests.
|
||||
* the per-signal contributions easy to reason about in tests and in the
|
||||
* shadow-mode parity dashboard.
|
||||
*/
|
||||
export function composeEvidence(signals: RawSignals): readonly ResolutionEvidence[] {
|
||||
const out: ResolutionEvidence[] = [];
|
||||
@@ -140,7 +141,7 @@ export function composeEvidence(signals: RawSignals): readonly ResolutionEvidenc
|
||||
|
||||
/**
|
||||
* Sum evidence weights and clamp to `[0, 1]`. Separate from `composeEvidence`
|
||||
* so tests can inspect the raw evidence list.
|
||||
* so tests and the parity dashboard can inspect the raw evidence list.
|
||||
*/
|
||||
export function confidenceFromEvidence(evidence: readonly ResolutionEvidence[]): number {
|
||||
let sum = 0;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Shadow-mode aggregation — per-language parity %, per-evidence-kind
|
||||
* breakdown of divergences. Consumed by the parity dashboard (RING2-PKG-5).
|
||||
*
|
||||
* Pure functions; no I/O. The harness persists per-run JSON; the dashboard
|
||||
* reads `.gitnexus/shadow-parity/latest.json` and renders.
|
||||
*
|
||||
* Related types — `ShadowAgreement`, `ShadowCallsite`, `ShadowDiff` — are
|
||||
* defined alongside `diffResolutions` in `./diff.ts` and re-exported
|
||||
* through the top-level `gitnexus-shared` barrel. Consumers import all
|
||||
* three from `gitnexus-shared`, not from this module.
|
||||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #918.
|
||||
*/
|
||||
|
||||
import type { SupportedLanguages } from '../../languages.js';
|
||||
import type { ResolutionEvidence } from '../types.js';
|
||||
import type { ShadowAgreement, ShadowDiff } from './diff.js';
|
||||
|
||||
// ─── Aggregated report shape ────────────────────────────────────────────────
|
||||
|
||||
export interface LanguageParityRow {
|
||||
readonly language: SupportedLanguages;
|
||||
readonly totalCalls: number;
|
||||
readonly bothAgree: number;
|
||||
readonly onlyLegacy: number;
|
||||
readonly onlyNew: number;
|
||||
readonly bothDisagree: number;
|
||||
readonly bothEmpty: number;
|
||||
/**
|
||||
* Fraction in [0, 1]. Numerator = `bothAgree`; denominator = "calls where
|
||||
* at least one side resolved" = `totalCalls - bothEmpty`.
|
||||
*
|
||||
* When the denominator is 0 (all calls for this language were
|
||||
* `both-empty`), returns 0. Callers rendering the dashboard should treat
|
||||
* a 0 parity alongside `totalCalls === bothEmpty` as "no signal" rather
|
||||
* than "total disagreement".
|
||||
*/
|
||||
readonly parity: number;
|
||||
/**
|
||||
* Divergence signals broken down by `ResolutionEvidence.kind`. Sourced
|
||||
* from `ShadowDiff.evidenceDelta` on non-agreeing rows only — `both-agree`
|
||||
* and `both-empty` do not contribute.
|
||||
*/
|
||||
readonly evidenceBreakdown: ReadonlyMap<ResolutionEvidence['kind'], number>;
|
||||
}
|
||||
|
||||
export interface ShadowParityReport {
|
||||
readonly generatedAt: string; // ISO 8601
|
||||
readonly perLanguage: readonly LanguageParityRow[];
|
||||
readonly overall: Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'>;
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Aggregate a stream of `ShadowDiff` records into a `ShadowParityReport`,
|
||||
* bucketed by language. Pure function.
|
||||
*
|
||||
* - `perLanguage` rows are sorted alphabetically by `SupportedLanguages`
|
||||
* value for stable JSON output (the dashboard reads
|
||||
* `.gitnexus/shadow-parity/latest.json` and diffing snapshots is useful).
|
||||
* - `overall` is the column-wise sum across languages.
|
||||
* - `generatedAt` is injected via the `now` parameter so tests stay
|
||||
* deterministic; production callers let it default to `new Date()`.
|
||||
*/
|
||||
export function aggregateDiffs(
|
||||
diffs: readonly { readonly language: SupportedLanguages; readonly diff: ShadowDiff }[],
|
||||
now: Date = new Date(),
|
||||
): ShadowParityReport {
|
||||
const perLanguageMap = new Map<SupportedLanguages, MutableCounts>();
|
||||
|
||||
for (const { language, diff } of diffs) {
|
||||
let counts = perLanguageMap.get(language);
|
||||
if (!counts) {
|
||||
counts = makeEmptyCounts();
|
||||
perLanguageMap.set(language, counts);
|
||||
}
|
||||
tallyDiff(counts, diff);
|
||||
}
|
||||
|
||||
const perLanguage: LanguageParityRow[] = Array.from(perLanguageMap.entries())
|
||||
.map(([language, counts]) => buildRow(language, counts))
|
||||
.sort((a, b) => a.language.localeCompare(b.language));
|
||||
|
||||
const overall = buildOverallRow(perLanguage);
|
||||
|
||||
return {
|
||||
generatedAt: now.toISOString(),
|
||||
perLanguage,
|
||||
overall,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
interface MutableCounts {
|
||||
totalCalls: number;
|
||||
bothAgree: number;
|
||||
onlyLegacy: number;
|
||||
onlyNew: number;
|
||||
bothDisagree: number;
|
||||
bothEmpty: number;
|
||||
evidenceBreakdown: Map<ResolutionEvidence['kind'], number>;
|
||||
}
|
||||
|
||||
function makeEmptyCounts(): MutableCounts {
|
||||
return {
|
||||
totalCalls: 0,
|
||||
bothAgree: 0,
|
||||
onlyLegacy: 0,
|
||||
onlyNew: 0,
|
||||
bothDisagree: 0,
|
||||
bothEmpty: 0,
|
||||
evidenceBreakdown: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function tallyDiff(counts: MutableCounts, diff: ShadowDiff): void {
|
||||
counts.totalCalls += 1;
|
||||
incrementAgreement(counts, diff.agreement);
|
||||
if (diff.agreement === 'both-agree' || diff.agreement === 'both-empty') return;
|
||||
for (const ev of diff.evidenceDelta) {
|
||||
counts.evidenceBreakdown.set(ev.kind, (counts.evidenceBreakdown.get(ev.kind) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function incrementAgreement(counts: MutableCounts, agreement: ShadowAgreement): void {
|
||||
switch (agreement) {
|
||||
case 'both-agree':
|
||||
counts.bothAgree += 1;
|
||||
return;
|
||||
case 'only-legacy':
|
||||
counts.onlyLegacy += 1;
|
||||
return;
|
||||
case 'only-new':
|
||||
counts.onlyNew += 1;
|
||||
return;
|
||||
case 'both-disagree':
|
||||
counts.bothDisagree += 1;
|
||||
return;
|
||||
case 'both-empty':
|
||||
counts.bothEmpty += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRow(language: SupportedLanguages, counts: MutableCounts): LanguageParityRow {
|
||||
const resolved = counts.totalCalls - counts.bothEmpty;
|
||||
const parity = resolved > 0 ? counts.bothAgree / resolved : 0;
|
||||
return {
|
||||
language,
|
||||
totalCalls: counts.totalCalls,
|
||||
bothAgree: counts.bothAgree,
|
||||
onlyLegacy: counts.onlyLegacy,
|
||||
onlyNew: counts.onlyNew,
|
||||
bothDisagree: counts.bothDisagree,
|
||||
bothEmpty: counts.bothEmpty,
|
||||
parity,
|
||||
// Freeze via `new Map` on a sorted-kind copy so downstream consumers
|
||||
// can't mutate the aggregator's internal state.
|
||||
evidenceBreakdown: new Map(
|
||||
Array.from(counts.evidenceBreakdown.entries()).sort(([a], [b]) => a.localeCompare(b)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function buildOverallRow(
|
||||
perLanguage: readonly LanguageParityRow[],
|
||||
): Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'> {
|
||||
let totalCalls = 0;
|
||||
let bothAgree = 0;
|
||||
let onlyLegacy = 0;
|
||||
let onlyNew = 0;
|
||||
let bothDisagree = 0;
|
||||
let bothEmpty = 0;
|
||||
for (const row of perLanguage) {
|
||||
totalCalls += row.totalCalls;
|
||||
bothAgree += row.bothAgree;
|
||||
onlyLegacy += row.onlyLegacy;
|
||||
onlyNew += row.onlyNew;
|
||||
bothDisagree += row.bothDisagree;
|
||||
bothEmpty += row.bothEmpty;
|
||||
}
|
||||
const resolved = totalCalls - bothEmpty;
|
||||
const parity = resolved > 0 ? bothAgree / resolved : 0;
|
||||
return { totalCalls, bothAgree, onlyLegacy, onlyNew, bothDisagree, bothEmpty, parity };
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Shadow-mode diff logic — RFC §6.3.
|
||||
*
|
||||
* Pure comparison logic for shadow mode. Takes two `Resolution[]` (legacy
|
||||
* DAG result + new scope-based registry result) and produces a structured
|
||||
* diff record for the parity dashboard.
|
||||
*
|
||||
* Consumed by the Ring 2 PKG shadow harness (#923), which dual-runs each
|
||||
* call through legacy + new paths, diffs results, and persists per-run JSON
|
||||
* for the parity dashboard.
|
||||
*
|
||||
* Part of RFC #909 Ring 2 SHARED — #918.
|
||||
*/
|
||||
|
||||
import type { Resolution, ResolutionEvidence } from '../types.js';
|
||||
|
||||
// ─── Diff record shape ──────────────────────────────────────────────────────
|
||||
|
||||
export type ShadowAgreement =
|
||||
| 'both-agree' // top match identical (same DefId)
|
||||
| 'only-legacy' // legacy resolved; new did not
|
||||
| 'only-new' // new resolved; legacy did not
|
||||
| 'both-disagree' // both resolved, but to different targets
|
||||
| 'both-empty'; // both returned empty
|
||||
|
||||
export interface ShadowDiff {
|
||||
readonly callsite: ShadowCallsite;
|
||||
readonly legacy: Resolution | null;
|
||||
readonly newResult: Resolution | null;
|
||||
readonly agreement: ShadowAgreement;
|
||||
/**
|
||||
* Symmetric difference of the two top resolutions' `evidence` arrays,
|
||||
* keyed on `ResolutionEvidence.kind`.
|
||||
*
|
||||
* - For `'both-agree'` and `'both-empty'` agreements, always empty.
|
||||
* - For `'both-disagree'`, contains evidence kinds present on exactly one
|
||||
* side (not in both).
|
||||
* - For `'only-legacy'`, contains all of legacy's top evidence.
|
||||
* - For `'only-new'`, contains all of new's top evidence.
|
||||
*/
|
||||
readonly evidenceDelta: readonly ResolutionEvidence[];
|
||||
}
|
||||
|
||||
export interface ShadowCallsite {
|
||||
readonly filePath: string;
|
||||
readonly line: number;
|
||||
readonly col: number;
|
||||
readonly calledName: string;
|
||||
}
|
||||
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compare two `Resolution[]` arrays (top matches at `[0]`) and produce a
|
||||
* `ShadowDiff`. Pure function.
|
||||
*
|
||||
* Agreement rules:
|
||||
* - both arrays empty → `'both-empty'`, `evidenceDelta: []`
|
||||
* - legacy empty, new non-empty → `'only-new'`, `evidenceDelta` = new's top evidence
|
||||
* - legacy non-empty, new empty → `'only-legacy'`, `evidenceDelta` = legacy's top evidence
|
||||
* - both non-empty, same top `def.nodeId` → `'both-agree'`, `evidenceDelta: []`
|
||||
* - both non-empty, different top `def.nodeId` → `'both-disagree'`,
|
||||
* `evidenceDelta` = symmetric difference by `ResolutionEvidence.kind`
|
||||
* (first occurrence of a kind-only-on-legacy then kind-only-on-new; order
|
||||
* preserved from input arrays)
|
||||
*
|
||||
* Evidence-delta rationale: callers aggregating divergences want to know
|
||||
* which signal kinds explain a disagreement. Keying on `kind` (not full
|
||||
* equality over `weight`/`note`) avoids spurious deltas when the same
|
||||
* signal fires with slightly different calibration weights on each side.
|
||||
*/
|
||||
export function diffResolutions(
|
||||
callsite: ShadowCallsite,
|
||||
legacy: readonly Resolution[],
|
||||
newResult: readonly Resolution[],
|
||||
): ShadowDiff {
|
||||
const legacyTop: Resolution | null = legacy.length > 0 ? legacy[0] : null;
|
||||
const newTop: Resolution | null = newResult.length > 0 ? newResult[0] : null;
|
||||
|
||||
const agreement: ShadowAgreement = (() => {
|
||||
if (legacyTop === null && newTop === null) return 'both-empty';
|
||||
if (legacyTop === null) return 'only-new';
|
||||
if (newTop === null) return 'only-legacy';
|
||||
return legacyTop.def.nodeId === newTop.def.nodeId ? 'both-agree' : 'both-disagree';
|
||||
})();
|
||||
|
||||
const evidenceDelta = computeEvidenceDelta(legacyTop, newTop, agreement);
|
||||
|
||||
return {
|
||||
callsite,
|
||||
legacy: legacyTop,
|
||||
newResult: newTop,
|
||||
agreement,
|
||||
evidenceDelta,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Internal helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Symmetric difference of two evidence arrays, keyed on
|
||||
* `ResolutionEvidence.kind`. Preserves input order: legacy-only signals
|
||||
* first (in legacy's original order), then new-only signals (in new's order).
|
||||
*
|
||||
* For `'both-agree'` / `'both-empty'` the delta is empty by contract. For
|
||||
* `'only-legacy'` / `'only-new'` one side's evidence is the delta (nothing to
|
||||
* subtract against).
|
||||
*/
|
||||
function computeEvidenceDelta(
|
||||
legacy: Resolution | null,
|
||||
newResult: Resolution | null,
|
||||
agreement: ShadowAgreement,
|
||||
): readonly ResolutionEvidence[] {
|
||||
if (agreement === 'both-agree' || agreement === 'both-empty') return [];
|
||||
if (agreement === 'only-legacy') return legacy!.evidence;
|
||||
if (agreement === 'only-new') return newResult!.evidence;
|
||||
|
||||
// both-disagree: symmetric difference keyed on `kind`
|
||||
const legacyKinds = new Set(legacy!.evidence.map((e) => e.kind));
|
||||
const newKinds = new Set(newResult!.evidence.map((e) => e.kind));
|
||||
|
||||
const onlyInLegacy = legacy!.evidence.filter((e) => !newKinds.has(e.kind));
|
||||
const onlyInNew = newResult!.evidence.filter((e) => !legacyKinds.has(e.kind));
|
||||
|
||||
return [...onlyInLegacy, ...onlyInNew];
|
||||
}
|
||||
@@ -59,13 +59,4 @@ export interface SymbolDefinition {
|
||||
isExplicit?: boolean;
|
||||
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
|
||||
ownerId?: string;
|
||||
/** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)
|
||||
* tagged during the C++ resolution phase. Lets the graph bridge retry a
|
||||
* namespace-prefixed node-lookup key and lets the qualified-base resolver
|
||||
* break same-tail cross-namespace inheritance ties. A deliberate sidecar,
|
||||
* separate from `qualifiedName`: it does NOT participate in graph node
|
||||
* identity (node keys derive from filePath/type/qualifiedName) and leaves the
|
||||
* qualifiedName-keyed resolution index untouched. Absent for the common case
|
||||
* (non-namespace-nested defs and all non-C++ languages). */
|
||||
namespacePrefix?: string;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ export const NODE_COLORS: Record<NodeLabel, string> = {
|
||||
Template: '#a78bfa', // Violet light - like Type
|
||||
Route: '#f43f5e', // Rose - like Process
|
||||
Tool: '#a855f7', // Purple - like Project
|
||||
BasicBlock: '#475569', // Slate darker - control-flow node (muted, taint/PDG substrate)
|
||||
};
|
||||
|
||||
// Node sizes by type - clear visual hierarchy with dramatic size differences
|
||||
@@ -80,7 +79,6 @@ export const NODE_SIZES: Record<NodeLabel, number> = {
|
||||
Template: 3, // Like Type
|
||||
Route: 5, // Like Enum
|
||||
Tool: 5, // Like Enum
|
||||
BasicBlock: 2, // Tiny - control-flow node (taint/PDG substrate)
|
||||
};
|
||||
|
||||
// Community color palette for cluster-based coloring
|
||||
|
||||
@@ -13,26 +13,6 @@ node_modules/
|
||||
vendor/**/node_modules
|
||||
vendor/**/build
|
||||
|
||||
# ── Lean publish (FUTURE optimization — NOT done here) ─────────────────────────
|
||||
# Once the build-tree-sitter-prebuilds workflow has committed 6/6 prebuilds for
|
||||
# EVERY vendored grammar (c, dart, proto, kotlin, swift), the ~50 MB of generated
|
||||
# source (parser.c etc.) can be dropped from the tarball — node-gyp-build never
|
||||
# needs the source when a prebuild matches.
|
||||
#
|
||||
# IMPORTANT: this CANNOT be done from this file. package.json's `files: ["vendor"]`
|
||||
# allow-list OVERRIDES .npmignore for the vendor/ subtree (verified: an active
|
||||
# `vendor/**/src/parser.c` line here does NOT exclude it from `npm pack`). To slim
|
||||
# the tarball, narrow the `files` field instead — replace the blanket "vendor"
|
||||
# with the non-source subpaths only (vendor/**/prebuilds/**,
|
||||
# vendor/**/bindings/node/index.*, vendor/**/src/node-types.json,
|
||||
# vendor/**/package.json, vendor/**/LICENSE, vendor/**/README.md).
|
||||
#
|
||||
# Whatever the mechanism, the prepack guard
|
||||
# (scripts/assert-publish-grammar-coverage.cjs, also `npm run
|
||||
# assert-publish-coverage`) inspects the EFFECTIVE `npm pack` file list and FAILS
|
||||
# the publish whenever a grammar with <6 prebuilds loses a source-build input — so
|
||||
# the slim can never silently ship a dead grammar. Do not bypass it.
|
||||
|
||||
# Package lock (consumers use their own)
|
||||
package-lock.json
|
||||
|
||||
|
||||
@@ -4,100 +4,6 @@ All notable changes to GitNexus will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.6.7] - 2026-06-09
|
||||
|
||||
### Added
|
||||
|
||||
- **Toolchain-free tree-sitter install** — the `c`, `dart`, `proto`, `kotlin`, and `swift` grammars now ship vendored native prebuilds (six platform/arch each — linux/darwin/win32 × x64/arm64, every `.node` load-and-parse verified with committed `SHA256SUMS` and SLSA build provenance), so a fresh install no longer requires a C/C++ toolchain; `kotlin` moved off its `optionalDependency` into the vendored path, `dart`/`proto` keep a source-build fallback when no prebuild matches, and a registry-parameterized CI workflow builds, load-validates, and vendors the binaries (#2113, #2125, #2110)
|
||||
- **`gitnexus uninstall`** — reverses `gitnexus setup` target-by-target, surgically removing GitNexus MCP server entries (Cursor, Claude Code, Antigravity, OpenCode, Codex), installed skill directories, and Claude Code / Antigravity hook entries with their bundled scripts; idempotent, JSONC-preserving, dry-run by default with `--force` to apply (#2062, #2060)
|
||||
- **MCP `list_repos` pagination** — bounded `limit`/`offset` paging so clients can reliably enumerate every indexed repository instead of having the unpaginated array truncated by LLM token limits; the result is now a `{ repositories, pagination }` object (page until `pagination.hasMore` is false), with deterministic `(lower-cased name, path)` ordering (#2120, #2119)
|
||||
- **C++ inheritance-lattice member lookup** — receiver members now resolve through the inheritance lattice with dominance hiding, ambiguous-base suppression, virtual-diamond deduplication, and overload ranking, and class-scope `using Base::member` declarations are no longer mistaken for namespace imports (#2077, #1891)
|
||||
- **Taint/PDG substrate (M0)** — foundational graph schema and pipeline seams for reliable taint analysis on a PDG-expandable substrate: the `BasicBlock` node label and `CFG` / `REACHING_DEF` / `TAINTED` / `SANITIZES` / `TAINT_PATH` relationship types (round-tripped through the bulk-COPY path), a phase-registry seam (`registerPhase` / `enabledWhen`) generalising the graph-phase opt-in guard, and a per-language source/sink/sanitizer config registry. All additive and inert — no phase emits the new nodes/edges yet and a default `analyze` run is byte-identical to before (#2092, #2080)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Optional grammars lazy-loaded so `analyze` never crashes when one is missing** — the swift/dart/kotlin `query.ts` modules no longer statically import their tree-sitter binding at module load, so a missing optional grammar can no longer abort `gitnexus analyze` (or the MCP server, `doctor`, and `.githooks` auto-reindex) with `ERR_MODULE_NOT_FOUND` regardless of the repo's actual languages; grammars now resolve lazily at first use inside the worker, `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` is honored at runtime, the scope-resolution phase excludes unavailable-language files, and skip diagnostics/precheck globs were corrected (#2101, #2091, #2093)
|
||||
- **`tree-sitter-kotlin` optional-grammar install** — install now fails soft when no C/C++ toolchain is present, emitting one clear warning and always exiting 0 (mirroring the Swift/Dart/Proto probes) instead of breaking `gitnexus` install; optional-grammar/toolchain docs corrected to include Kotlin (#2110, #2107)
|
||||
- **CLI image FTS keyword search** — the full-text-search extension is now baked into the CLI Docker image so a containerized `serve` does offline keyword search instead of silently degrading to vector-only (#2108)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Tree-sitter prebuild CI matrix greened and made re-run-safe** — dropped the broken `-t 22` flag from the `prebuildify` invocation that crashed every matrix job (`v.indexOf is not a function`; N-API prebuilds are Node-version-agnostic, so no target is needed) (#2121), cleared npm-bundled `prebuilds/` before prebuildify so the host tuple is detected (not a stray `win32-x64`) and source-built the `tree-sitter` runtime peer on `linux-arm64` where upstream ships no prebuild (#2122), and switched the vendor-prebuilds push to `git push --force` so re-running a workflow no longer fails with a stale-lease rejection (#2123)
|
||||
|
||||
### Performance
|
||||
|
||||
- **MCP `query` enrichment batched** — the `query` tool now batches its per-symbol enrichment lookups (3N sequential pool round-trips collapsed to 2–3 `WHERE n.id IN $nodeIds` queries), cutting N+1 round-trips with byte-identical output (#2108)
|
||||
|
||||
### Chore / Dependencies
|
||||
|
||||
- **`@ladybugdb/core` bumped 0.17.0 → 0.17.1 in /gitnexus** (#2098)
|
||||
- **Claude plugin manifests synced to the release version** — bumped `plugin.json` and the `gitnexus` `marketplace.json` entry to match the published npm version (stale `1.3.x` manifests had blocked marketplace updates), added a Vitest guard asserting all three manifests advertise one version, and documented the sync step in `CONTRIBUTING.md` (#2090)
|
||||
|
||||
## [1.6.6] - 2026-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Scope-resolution (RFC #909) migrations completed across the language matrix** — Rust (#1639), JavaScript (#1640), Ruby (#1831), Swift (#937, #1948), Vue SFC (#940, #1950), Dart (#939, #1970), COBOL (#941, #1835, #1842), and Kotlin (#1727, #1746, #1782) now run on the registry-primary path; Java reached 100% scope-resolution parity and joined `MIGRATED_LANGUAGES` (#1805); per-language progress reporting added to the scope-resolution phase (#1813)
|
||||
- **HTTP route & consumer contract extraction (group mode)** — Spring interface routes attributed to controllers (#1743); named/positional Java Spring route args (#1834); Kotlin Spring HTTP route, consumer, and WebClient long-form extraction (#1849, #1855, #1884); Java HTTP consumer contracts (#1872); OpenFeign `@RequestLine` consumer contracts incl. plain interfaces without `@FeignClient` (#1904, #1917); FastAPI `include_router(prefix=...)` cross-file routes (#1877); indirect call patterns via FastAPI `Depends()` and frontend HTTP consumers (#1852); gRPC consumer FQN derivation from Java imports for client-jar consumers (#1889)
|
||||
- **C++ overload & template resolution** — operator-call resolution (#1754), template partial ordering (#1885), user-defined conversion ranking (#1829), nullptr/ellipsis pointer conversion ranks (#1708), SFINAE filter (#1623), expanded `type_traits` constraint registry (#1648), structured resolver-suppression outcomes (#1785), function-type ADL entities (#1822), and a parameter-type class sidecar (#1642)
|
||||
- **Go enhancements** — structural interface implementation inference (#1966) and a `builtInNames` set for the Go language provider (#1886)
|
||||
- **Self-healing worker pool** — automatic worker replacement plus deferred-resolution observability and verbose progress logging (#1741, #1773, #1947)
|
||||
- **`.gitnexusrc` config file and `gitnexus analyze --default-branch`** (#243, #1996)
|
||||
- **CLI / MCP impact ergonomics** — `--uid/--file/--kind` disambiguation flags (#1907, #1914), `limit/offset/summaryOnly` pagination on the impact tool (#1818), and a per-symbol `processes` field on `byDepth` items (#1867)
|
||||
- **`gitnexus analyze --repair-fts`** — enforces FTS verification with hardened repair safeguards (#1720)
|
||||
- **Web viewer** — Tree View and Circles View (#1799), GitLab repository URLs (#1565), `GITNEXUS_BACKEND_URL` env var for Docker deployments (#1286), and web + CLI internationalization (#1748)
|
||||
- **Wiki** — local Claude/Codex providers (#1769), an opencode local provider (#2039), and `gitnexus wiki --lang <lang>` for multilanguage wiki generation (#1613)
|
||||
- **`detect-changes` git-worktree support** (#1654)
|
||||
- **DeepSeek V4 API support** (#1594)
|
||||
- **Devcontainer for the Claude / Codex / Cursor CLIs** (#1875) and antigravity integration setup + hook adapter (#1730)
|
||||
- **Object-literal methods linked to exported bindings** (#1718)
|
||||
- **`eval-server --host`** for a user-configured bind IP (#1667)
|
||||
- **PR reviewer swarm agents** (#1851)
|
||||
- **tree-sitter node-type/field validation gate** — validates against the grammar and removes dead literal handling (#1937)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Parsing-layer coverage gaps closed across the language matrix** (umbrella #1919) — remaining open gaps (#2072) plus Java F35/F38/F41 (#1928, #2045), PHP F53/F54/F55 (#1931, #1989), COBOL F17–F23 (#1925, #1959), Rust F66/F68/F71/F72 (#1934, #1974), Python F57/F58/F61 (#1932, #1964), JS/TS F44/F83/F85/F86/F87 (#1929, #1968), and Ruby F62 (#1933, #1972)
|
||||
- **Fully-qualified nested-type identity for C++ and Ruby** — distinct nodes for union-, anonymous-namespace-, and same-tail-nested types (#1978, #1981, #2004, #2005); cross-namespace same-tail inheritance bases resolved (#1993, #2005); Ruby same-tail nested mixin modules qualified with `IMPLEMENTS` routed by scope (#1991, #2006); shared codec for `__heritage__`/`__property__` markers (#1994, #2007); graph nodes materialized for scoped class/module/impl declarations (#1975, #1977); generic Rust inherent-impl methods owned through the mod-qualified `Impl` node (#1992, #2003)
|
||||
- **C# resolution & memory** — global-namespace `typeBindings` O(files²) OOM eliminated (#1871, #1954) and namespace-siblings OOM with worker-path re-parse removed (#1905); qualified/alias constructor names, `:base`/`:this` initializers, and generic type-arg stripping (#2046); primary-base receiver type normalization (#2036); spurious `IMPORTS` edges from ungated `using` resolution stopped (#1881, #1908)
|
||||
- **C++ dependent-base and member lookup** — resolution across nested/inline namespaces (#1634, #1814), base-specifier qualifier threading (#1815, #1819), call-site types threaded into qualified member lookup (#1632, #1810), variadic pack dependent lookup (#1909), uninitialized multi-declarators (#1965), and typedef-enum / anonymous-struct declarations (#1941)
|
||||
- **Kotlin type resolution** — smart-cast refinement for `when/is` and `if/is` (#1758, #1774), overload target-id by parameter types (#1761, #1777), cross-file iterable return propagation (#1759, #1775), method-chain fixpoint receiver types (#1760, #1776), virtual dispatch via constructor type override (#1762, #1778), interface default-method dispatch via implements-split MRO (#1763, #1779), and default-parameter arity detection (#2034)
|
||||
- **Go declarations** — multi-name declaration capture (#2032), fixed-array parameter binding normalization (#1988), and generic composite-literal constructor inference F33 (#1976)
|
||||
- **Rust / PHP / Vue / Java parsing** — Rust `struct_expression` name pattern split (#2051); PHP import decomposition, namespace-less `.phtml` module scopes, and Blade-template exclusion (#1801, #1790, #1989); Vue JSDoc, dual-script merge, and lang plumbing F89/F90/F92 (#1936, #2050); Java inherited `RequestMapping` prefix deduplication (#2057) and same-module type resolution for duplicate FQNs (#1712)
|
||||
- **TypeScript** — HOC pattern false positives fixed with `export default` HOC support (#1943) and suffix-index reuse in the scope resolver (#1840)
|
||||
- **Inheritance on the worker path** — all languages' inheritance migrated to scope-resolution in worker mode (#1951, #1956); centralized heritage supertype matching (#1921, #1922, #1940); `File->Member` `DEFINES` edges skipped for class members (#1949); phantom `Function` defs for array-method callbacks no longer emitted (#1906)
|
||||
- **MCP** — sibling-clone repo-ID collisions prevented and generated MCP tool names corrected (#2067); orphan processes avoided by handling stdin close/end and the startup race (#2049); duplicate-name repo resolution disambiguated for worktrees (#1753); Windows setup fallback when global `gitnexus` resolves to a non-spawnable shim (#1694)
|
||||
- **Worker pool** — resilient zero-copy ingestion worker pool prevents analyze hangs on TS-root-scale loads (#1693); cache-hit native workers no longer abort (#1751, #1833); worker-pool docs drift corrected and worker-side stack surfaced on crash (#2068, #2070)
|
||||
- **LadybugDB** — FTS loaded in the Windows read pool (#2040) and probed-then-loaded on Windows (#1690, #1692); non-ASCII KuzuDB paths resolved on Windows (#1811, #1817); WAL corruption detected in schema init with recovery surfaced (#1647, #1650); WAL checkpoint-threshold control (#1772); init lock skipped for read-only opens (#1783, #1784); `serve` kept stable when sidecars are missing (#1747)
|
||||
- **Server / API** — `gitnexus serve` startup restored under Express 5 (#1749); `/api/graph`, `/api/search`, `/api/grep` opened read-only (#1686); native read-only enforcement and prepared statements for Cypher query paths (#1655); `eval-server` localhost binding left to the OS (#1722)
|
||||
- **Embeddings** — local ONNX runtime guarded on macOS Intel before the transformers.js import (#1987)
|
||||
- **Web agent** — Nexus AI agent system prompt aligned with registered tools (#1984) and the agent stopped cleanly on user Stop (#1820)
|
||||
- **Group / contracts** — HTTP graph and source contracts unioned (#1709); `httpx` `AsyncClient` alias imports detected (#1687); Node gRPC `loadPackageDefinition` gate no longer matches every member call (#1916); manifest/workspace extraction moved before `closeLbug` (#1802, #1807)
|
||||
- **Hooks / install** — `gitnexus` resolved on `PATH` via a pure-Node, all-OS scan (#1938, #1980); offline-first extension installs (#1161); actionable error and docs for the `pnpm dlx`/`pnpx` native-load crash (#307, #1967); `onnxruntime-common` declared as a runtime dependency (#2074); vendored grammars materialized to fix Windows EPERM (#1728, #1729)
|
||||
- **CLI** — missing LadybugDB native binary detected at startup with actionable guidance (#835, #1837); `--no-stats` applied to the keep-marker stats line (#1706, #1765); skipped large-file paths surfaced by default (#1659, #1661); build.js skipped when running outside the monorepo (#1795, #1816); auto-heap raised to 16 GB with tightened cross-platform OOM guidance for UE5-scale repos (#1652)
|
||||
- **Wiki** — hidden 60s default timeout removed with timeout/retry flag validation and surfaced timeout errors (#1651); budget-aware grouping to prevent context overflow on large repos (#627, #1832)
|
||||
- **`detect-changes`** — `resolveWorktreeCwd` guarded against overriding a separately-indexed worktree (#1691)
|
||||
- **Windows reliability** — `windowsHide:true` passed to every `child_process` spawn-family call (#1794)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Legacy resolution deletion (Ring 4)** — removed the legacy call-resolution DAG + heritage processor (RING4-1, #942, #2023), the legacy resolution-context + tiered-lookup plumbing (RING4-2, #943, #2033), and the shadow-mode parity harness (RING4-3, #944, #2071)
|
||||
- **CONTRIBUTING** — clarified local development setup (#2024)
|
||||
- **Tests / CI** — cli-e2e made read-only and eval-server tests hardened under load (#2000, #1786, #1838, #1688); parity shards consolidated and the cross-platform matrix narrowed (#1798); devcontainer smoke build hardened against Docker Hub flakes (#1969); gitleaks stabilized (#2027)
|
||||
|
||||
### Performance
|
||||
|
||||
- **Linux-kernel-scale analysis overhaul** — worker-pool parse, finalize O(n²), and the scope-resolution memory wall (#1983, #2038)
|
||||
- **Scope-capture linearized across all languages (O(n²)→O(n))** plus Python import-resolution linearization (#1918), the Go-specific re-walk fix (#1848, #1915), and owner-keyed lookup for Step 2 member resolution (#1657)
|
||||
- **C++ ADL candidates indexed once instead of per-site rescans** (#1990)
|
||||
- **Inert local value symbols pruned** during ingestion (#2065)
|
||||
|
||||
### Chore / Dependencies
|
||||
|
||||
- `@ladybugdb/core` bump in /gitnexus (#2056)
|
||||
- Routine dependency bumps across /gitnexus, /gitnexus-web, /eval, and GitHub Actions — incl. `hono`, `vitest`, `@vitest/coverage-v8`, `tsx`, `lru-cache`, `express`/`@types/express`, `express-rate-limit`, `qs`, `node-addon-api`, `brace-expansion`, `langchain`, `i18next`, `dompurify`, `lucide-react`, `axios`, `zod`, `@langchain/langgraph`, `@vercel/node`, `langsmith`, `aiohttp`, `idna`, and the `docker/*` / `github/codeql-action` / `release-drafter` / `dependency-review-action` actions (#2056, #2044, #2043, #2042, #2016, #2015, #2013, #2012, #2011, #2010, #2009, #2008, #2018, #2019, #2017, #2020, #1986, #1911, #1864, #1863, #1861, #1860, #1866, #1844, #1845, #1826, #1825, #1824, #1791, #1789, #1768, #1767, #1739, #1740, #1738, #1736, #1735, #1734, #1731, #1713, #1698, #1697, #1696, #1689, #1604, #1552, #1464, #872)
|
||||
- **Security** — `@vercel/node` upgraded in /gitnexus-web with transitive advisories remediated (#1705)
|
||||
|
||||
## [1.6.5] - 2026-05-16
|
||||
|
||||
### Added
|
||||
|
||||
+2
-15
@@ -126,7 +126,7 @@ Your AI agent gets these tools automatically:
|
||||
|
||||
| Tool | What It Does | `repo` Param |
|
||||
| ---------------- | ---------------------------------------------------------------- | ------------ |
|
||||
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) | — |
|
||||
| `list_repos` | Discover all indexed repositories | — |
|
||||
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF) | Optional |
|
||||
| `context` | 360-degree symbol view — categorized refs, process participation | Optional |
|
||||
| `impact` | Blast radius analysis with depth grouping and confidence | Optional |
|
||||
@@ -159,7 +159,6 @@ Your AI agent gets these tools automatically:
|
||||
|
||||
```bash
|
||||
gitnexus setup # Configure MCP for your editors (one-time)
|
||||
gitnexus uninstall # Preview removal of GitNexus MCP/skills/hooks (add --force to apply)
|
||||
gitnexus analyze [path] # Index a repository (or update stale index)
|
||||
gitnexus analyze --repair-fts # Fast path: rebuild/verify only FTS indexes on existing index data
|
||||
gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS rebuild
|
||||
@@ -197,8 +196,6 @@ gitnexus group query <name> <q> # Search execution flows across all repos in a
|
||||
gitnexus group status <name> # Check staleness of repos in a group
|
||||
```
|
||||
|
||||
> **`gitnexus uninstall`** reverses `gitnexus setup` — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified **by bundled gitnexus skill name** (e.g. `gitnexus-cli/`), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass `--force` to apply. Per-repo indexes (`gitnexus clean --all`) and the global npm package (`npm uninstall -g gitnexus`) are left for you to remove.
|
||||
|
||||
## Remote Embeddings
|
||||
|
||||
Set these env vars to use a remote OpenAI-compatible `/v1/embeddings` endpoint instead of the local model:
|
||||
@@ -403,7 +400,7 @@ Values above **32768 KB (32 MB)** are clamped to the tree-sitter parser ceiling;
|
||||
|
||||
### Analyze reports a worker timeout
|
||||
|
||||
Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and quarantines a file that repeatedly crashes its worker (respawning the slot so the pool keeps going). If a large repository needs more time per worker job, use either:
|
||||
Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and falls back to the sequential parser when needed. If a large repository needs more time per worker job, use either:
|
||||
|
||||
```bash
|
||||
# CLI flag, in seconds
|
||||
@@ -426,16 +423,6 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
|
||||
| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. |
|
||||
| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. |
|
||||
|
||||
### Graph cleanup tuning
|
||||
|
||||
After scope resolution, analyze prunes inert block-local value symbols (a function-local `const`/`let`/`var` that ends up with only its structural `File→DEFINES` edge) to keep the graph focused on cross-symbol relationships. Module/file-scope symbols, class members, and any local with a real edge are always kept.
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
|
||||
|
||||
Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
|
||||
|
||||
## Privacy
|
||||
|
||||
- All processing happens locally on your machine
|
||||
|
||||
@@ -70,17 +70,10 @@ this doc, run it under instrumentation:
|
||||
```bash
|
||||
# From the gitnexus/ subdir:
|
||||
cd gitnexus
|
||||
# The worker pool is the sole parse path, so every run needs the dist worker
|
||||
# (`npm run build`) and a pool size pinned via GITNEXUS_WORKER_POOL_SIZE.
|
||||
# Single-threaded baseline (sequential fallback):
|
||||
npx vitest run test/integration/parse-impl-large-fixture.test.ts --reporter=verbose
|
||||
|
||||
# Single-worker-pool baseline (closest analog to the old single-threaded run —
|
||||
# sequential parsing was removed, so a 1-worker pool is the floor):
|
||||
npm run build && \
|
||||
GITNEXUS_WORKER_POOL_SIZE=1 \
|
||||
GITNEXUS_VERBOSE=1 \
|
||||
npx vitest run test/integration/parse-impl-large-fixture.test.ts --reporter=verbose
|
||||
|
||||
# Multi-worker path:
|
||||
# Worker-pool path (requires built dist/ — pre-built by `npm run build`):
|
||||
npm run build && \
|
||||
GITNEXUS_WORKER_POOL_SIZE=4 \
|
||||
GITNEXUS_PARSE_CHUNK_CONCURRENCY=2 \
|
||||
@@ -104,26 +97,22 @@ node --inspect=0 \
|
||||
## Latest measurement
|
||||
|
||||
> _No measurement data has been collected yet — this file is the
|
||||
> methodology + harness scaffold. The U6 smoke test confirms the
|
||||
> worker-pool path stays well within its wall-clock budget, but every
|
||||
> throughput/heap cell below is a `_TBD_` placeholder for a future
|
||||
> bench-pass._
|
||||
> methodology + harness scaffold. The single recorded data point is the
|
||||
> U6 wall-clock smoke baseline below; the worker-pool rows are
|
||||
> placeholders for future bench-pass output._
|
||||
|
||||
The U6 integration test (`gitnexus/test/integration/parse-impl-large-fixture.test.ts`)
|
||||
runs the worker pool — the sole parse path now that sequential parsing
|
||||
has been removed (disabling the pool on a repo with parseable files
|
||||
raises a hard `WorkerPoolDisabledError`). It completes the synthetic
|
||||
fixture well within the 30 s `Promise.race` wall-clock budget on the
|
||||
development machine, but no worker-pool throughput/heap numbers have been
|
||||
captured yet, so the rows below are all `_TBD_`. (An earlier ~6 s figure
|
||||
recorded here was measured on the now-removed sequential path; it has
|
||||
been dropped rather than relabelled as a worker-pool baseline, since the
|
||||
two paths are not comparable.)
|
||||
was observed completing the synthetic fixture in **~6 seconds** under
|
||||
the sequential path (`skipWorkers: true`) on the development machine,
|
||||
well under the 30 s `Promise.race` wall-clock budget. That number is a
|
||||
smoke baseline only — recorded here for reference, not as a regression
|
||||
target.
|
||||
|
||||
| Path | files/s | wall-clock | peak heap | chunks | quarantined |
|
||||
| ------------------------------------------------------------------------- | ------- | ---------- | --------- | ------ | ----------- |
|
||||
| Worker pool, `--workers 1` (`GITNEXUS_WORKER_POOL_SIZE=1`), concurrency 1 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
|
||||
| Worker pool, `--workers 4`, concurrency 2 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
|
||||
| Path | files/s | wall-clock | peak heap | chunks | quarantined |
|
||||
| ------------------------------------------ | ------- | -------------------- | --------- | ------ | ----------- |
|
||||
| Sequential (`skipWorkers: true`, U6 smoke) | _TBD_ | ~6 s _(observation)_ | _TBD_ | 17 | 0 |
|
||||
| Worker pool, `--workers 4`, concurrency 2 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
|
||||
| Worker pool, `--workers 1`, concurrency 1 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
|
||||
|
||||
**Hardware:** _TBD — record OS, CPU, RAM, Node version, gitnexus SHA at
|
||||
the time of the bench-pass that populates the table above._
|
||||
|
||||
@@ -1 +1 @@
|
||||
c03f87cd8cd1ee716dea93cceb27109ee5b4584bc9fe426eea3f18fd6f9854cc
|
||||
06687dff942d531c4d453b5906a8666c90db4867eb43ed18304aa59a8a93ef9d
|
||||
|
||||
@@ -11,76 +11,71 @@
|
||||
"_note": "Updated for F17-F23 fixes (P2: TIMES guard, ADD GIVING, SQL AS alias). See PR #1959."
|
||||
},
|
||||
"c": {
|
||||
"fingerprint": "12a196b2d6249c8d86a931b12ecebc2a0cdf8d6f47683acdd0d8e9d8bc7657f5",
|
||||
"fingerprint": "0de009bdbfe095f530fa87eb32bce6ab83092c904f26b3c8fe8d8ab587cf6dc9",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance — flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96.",
|
||||
"_note": "#1983: + c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c — worker-path static-linkage side-channel test). Pure fixture-corpus drift: no c/captures.ts or query change branch-vs-main, existing fixtures' captures byte-identical (c-captures.test.ts 45/45), scaling stays linear (~0.97). The baseline was missed when the fixture landed; regenerated here. fingerprint 0de009b->39f3a83.",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance \u2014 flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96."
|
||||
},
|
||||
"cpp": {
|
||||
"fingerprint": "f56625342f73e182170e2c964d538e316c079fa6e9466a7f076bff2ebcf8aac4",
|
||||
"fingerprint": "538e8beebf0a69f6170dff452da3f98046a08cbe8b098b3c9943c4a8a79d2e22",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae. #2077 review follow-up: cpp-member-lattice adds cross-file, qualified-base, nested-template, inherited-using, this-receiver, and non-virtual-override regressions; fixture_count 274->275. Capture scaling remains linear (1.134 < 1.5)."
|
||||
"_rebaselined": "#1965 / #1923 F4: uninitialized non-leading multi-declarators now emit @declaration.variable captures; cpp-adl-inner-callable-outer-noncallable data::Pair a, b adds the legitimate fixture drift. Linear (~1.06).",
|
||||
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267."
|
||||
},
|
||||
"csharp": {
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
|
||||
"fingerprint": "2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9",
|
||||
"scaling_budget": 1.5,
|
||||
"_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11)."
|
||||
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.)",
|
||||
"fingerprint": "68ef32c126d5c6de5d8184c6ad0a6104043036daf9805947db8b21741b883f43",
|
||||
"scaling_budget": 1.5
|
||||
},
|
||||
"rust": {
|
||||
"fingerprint": "ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82",
|
||||
"fingerprint": "56ffc1c069af10cac3c82a32f3d148322ea570e116ebaae67315445f05407fef",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
|
||||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures — pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f."
|
||||
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical.",
|
||||
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126)."
|
||||
},
|
||||
"php": {
|
||||
"fingerprint": "bc2c27c5ba26d5aea61142a2a99fb772222f5b969205260eb7a71b4c0bd73cdb",
|
||||
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04).",
|
||||
"_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class — fixture count 138→140, fingerprint drift expected."
|
||||
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04)."
|
||||
},
|
||||
"ruby": {
|
||||
"fingerprint": "b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753",
|
||||
"fingerprint": "bf6b13a366e4116da3772f9a9fdd50517eb11da73918451392e014a2c905b2dd",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
|
||||
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) — pure fixture-corpus drift, scope-extractor captures unchanged; 81→82. #1991: + ruby-nested-mixin-tail-collision fixture (85→86). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb."
|
||||
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.)",
|
||||
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) — pure fixture-corpus drift, scope-extractor captures unchanged; 81→82."
|
||||
},
|
||||
"swift": {
|
||||
"fingerprint": "180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998",
|
||||
"fingerprint": "53325c6345161c5a495f997297af5a24fb718fd3e6647040160f8ab2a2c8e4c0",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
|
||||
"_rebaselined": "#1956: swift-qualified-base fixture + heritage-bearing scale source (class: Base, Serviceable \u2014 extends + protocol conformance); linear (~1.03)."
|
||||
},
|
||||
"dart": {
|
||||
"fingerprint": "94bf2c26e1ba96f4211634aa572c0a989b503e717e75dfc5df04f66c417de80f",
|
||||
"fingerprint": "a9e882b537765e8fd0ddfcd33b38b253dd86fc5ddffa6e4bf5a85ed8ee615eaa",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#939: dart added to the scope-capture bench with the registry-primary migration. Heritage-bearing scale source (Entity extends Base implements Marker) gates the @reference.inherits synth + the postfix-chain reference walk at scale. emitDartScopeCaptures threads tree-sitter captured nodes (no findNodeAtRange root-walk), so it is linear (~1.0).",
|
||||
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
|
||||
"_rebaselined": "#1970 review + tri-review follow-ups: constructor-call retag, cascade calls, built-in suppression, enum scope, #1926 F24/F25, named-ctor dedup (crash fix), container-name binding suppression; heritage file-affinity resolution. Fixtures: member-call-contexts, constructor-body, named-constructor-body, heritage-name-collision, construct-cascade."
|
||||
},
|
||||
"java": {
|
||||
"fingerprint": "9b29cafe32873b4902bda311bd089ffc04efe08f13557b966d29544be514080a",
|
||||
"fingerprint": "b63f9be458f7ece854e7b007159d7bf65b4b66a86e83a6c0656fc93ebd5d83da",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
|
||||
"_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box<T>()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06)."
|
||||
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.)"
|
||||
},
|
||||
"typescript": {
|
||||
"fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures — fingerprint drift expected.",
|
||||
"_note": "#1968: F44, F85, F87 — fingerprint drift expected."
|
||||
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures \u2014 fingerprint drift expected.",
|
||||
"_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected."
|
||||
},
|
||||
"javascript": {
|
||||
"fingerprint": "d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8",
|
||||
"fingerprint": "a8ddfb15620ae55e50651fc21ab14c4a1f874d9b19e208cc6cbf0a8daac8ec5b",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (extends Base); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
|
||||
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged."
|
||||
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05)."
|
||||
},
|
||||
"kotlin": {
|
||||
"fingerprint": "90aa832978d9744e50058e77a04748390a7e34e36b309f6c1d178eb07280b7ea",
|
||||
"fingerprint": "5121a11855cd9cc44a357ae3ff50953de80cdd743f00e8924c31503b132bcd84",
|
||||
"scaling_budget": 1.5,
|
||||
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
|
||||
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
|
||||
"_rebaselined": "#1956 synth-widening: + kotlin-qualified-base fixture; synthesizeKotlinInheritanceReferences now handles the explicit_delegation form (class F : Iface by d -> Iface), matching the #1940 legacy leg, at parity. Linear (~0.87)."
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+211
-355
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.7",
|
||||
"version": "1.6.6-rc.132",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.7",
|
||||
"version": "1.6.6-rc.132",
|
||||
"hasInstallScript": true,
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^4.1.0",
|
||||
"@ladybugdb/core": "^0.17.0",
|
||||
"@ladybugdb/core": "^0.16.1",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -26,15 +26,14 @@
|
||||
"ignore": "^7.0.5",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lru-cache": "^11.0.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"onnxruntime-common": "^1.26.0",
|
||||
"onnxruntime-node": "^1.24.0",
|
||||
"pandemonium": "^2.4.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"tree-sitter": "0.21.1",
|
||||
"tree-sitter-c": "0.21.4",
|
||||
"tree-sitter-c-sharp": "0.23.1",
|
||||
"tree-sitter-cpp": "0.23.2",
|
||||
"tree-sitter-go": "^0.23.0",
|
||||
@@ -65,6 +64,11 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"tree-sitter-kotlin": "^0.3.8"
|
||||
}
|
||||
},
|
||||
"../gitnexus-shared": {
|
||||
@@ -1155,28 +1159,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.17.1.tgz",
|
||||
"integrity": "sha512-K1bHnQrRy3bxkyrFHlxGqKUyIUS1LsRXKOSt14XGY/msBZHaDat/uBrlHiWpM4/24OtfOq/qwTqcTCXannnEjw==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.16.1.tgz",
|
||||
"integrity": "sha512-qwuEcR8CVMKb6tNDaHtq7Ux8hT/XbPC0db+vwutX6JxNAejyx7YomHKPSy9XAKURhYK8mezZe3UN8rf+xpHOjQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"apache-arrow": "^21.1.0",
|
||||
"cmake-js": "^8.0.0",
|
||||
"node-addon-api": "^6.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ladybugdb/core-darwin-arm64": "0.17.1",
|
||||
"@ladybugdb/core-darwin-x64": "0.17.1",
|
||||
"@ladybugdb/core-linux-arm64": "0.17.1",
|
||||
"@ladybugdb/core-linux-x64": "0.17.1",
|
||||
"@ladybugdb/core-win32-x64": "0.17.1"
|
||||
"@ladybugdb/core-darwin-arm64": "0.16.1",
|
||||
"@ladybugdb/core-darwin-x64": "0.16.1",
|
||||
"@ladybugdb/core-linux-arm64": "0.16.1",
|
||||
"@ladybugdb/core-linux-x64": "0.16.1",
|
||||
"@ladybugdb/core-win32-x64": "0.16.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core-darwin-arm64": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.17.1.tgz",
|
||||
"integrity": "sha512-JG/uzmolEh3wXJ/ME1EaTH5LTDQ9Cs+Q3Czul8pW2eWbWQZghQU3jjM++7ST7Bla5BX/WITqwPqPoC+sL+slfA==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.16.1.tgz",
|
||||
"integrity": "sha512-Nl+Cf70rD+HaC9IBHv+oeUwqX9plghXD7PN9tyMzMohRVPvcGEbqWPB6YcdJa8rR7qRqCCbmaNMDen5wg4rY2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1187,9 +1190,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@ladybugdb/core-darwin-x64": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.17.1.tgz",
|
||||
"integrity": "sha512-Enjm+/V9/jpKmtzF2PB0muVkgpFUGHEvA7r16eJWxVRA/BeO8VPmngTKy9rf/4Yc6TWexjoHRug04BbTXEmerg==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.16.1.tgz",
|
||||
"integrity": "sha512-4eAjfimAAQRSmDfUUkGrl9OhefxcW1ziA9tl0eljBlGoUseE7dL02+RSqjGohYMcQ+lzuHAq1QWb0XRlMA8YTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1200,9 +1203,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@ladybugdb/core-linux-arm64": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.17.1.tgz",
|
||||
"integrity": "sha512-P+xM9o4I3JAQtXpX19ZuLj9EeO2gppa+IdmAqhpI8tuhyA3/a85Eaxby1fXOjsbrnOAEyFJczUdyoDkhCPSyiw==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.16.1.tgz",
|
||||
"integrity": "sha512-zkctksev+hsPFrNxHHdq4lYK5OWdLhWfRdQzjzkgDyaHayHU6yCL2fgD6uPGQ8TRQ6/2DxMErb4p3FzGW85Ubw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1213,9 +1216,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@ladybugdb/core-linux-x64": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.17.1.tgz",
|
||||
"integrity": "sha512-N2ujE0CrsToBpVBpou1iWwEkK7CgVxucnUNxteySrnDccZwICXFP5BlcFpKE0qq3Eqmqszh4ptR4GuSi6rKPGw==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.16.1.tgz",
|
||||
"integrity": "sha512-5rAb9T5vif8WKhHwhobosu2/aiOwJkWb/ViybvUc5GFKunKl8VI6RmZQVeufT9zUzRktUwrxBrxblCxsnamXJw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1226,9 +1229,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@ladybugdb/core-win32-x64": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.17.1.tgz",
|
||||
"integrity": "sha512-9i3xNfFAMqFRuQG3F1hOCWYGna6eTg8HJ/XYhWVDGkeFJNUV3IdneEiYttF5B2qAtQYUd4sAikScsImrMRw+6g==",
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.16.1.tgz",
|
||||
"integrity": "sha512-ShOUTrIuZKQ63J95tcRJxKf1cvg8yi2FSYx9kMTSercc1FdQZPV+zxUN0myMq3MTWOl7xDxsVMmdp/t80O29UQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1304,9 +1307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"version": "0.132.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
|
||||
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1384,9 +1387,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1401,9 +1404,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1418,9 +1421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
|
||||
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1435,9 +1438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
|
||||
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1452,9 +1455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
|
||||
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1469,9 +1472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1486,9 +1489,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1503,9 +1506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1520,9 +1523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1537,9 +1540,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
|
||||
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1554,9 +1557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
|
||||
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1571,9 +1574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
|
||||
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1588,9 +1591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
|
||||
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -1618,9 +1621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1635,9 +1638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
|
||||
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1672,15 +1675,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
@@ -1724,18 +1718,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/command-line-args": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
|
||||
"integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/command-line-usage": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
|
||||
"integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
@@ -1865,14 +1847,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz",
|
||||
"integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz",
|
||||
"integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/utils": "4.1.7",
|
||||
"ast-v8-to-istanbul": "^1.0.0",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
@@ -1886,8 +1868,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.1.8",
|
||||
"vitest": "4.1.8"
|
||||
"@vitest/browser": "4.1.7",
|
||||
"vitest": "4.1.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1896,16 +1878,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
|
||||
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
|
||||
"integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/spy": "4.1.7",
|
||||
"@vitest/utils": "4.1.7",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
@@ -1914,13 +1896,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
|
||||
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz",
|
||||
"integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/spy": "4.1.7",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -1941,9 +1923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
|
||||
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz",
|
||||
"integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1954,13 +1936,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
|
||||
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz",
|
||||
"integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/utils": "4.1.7",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1968,14 +1950,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
|
||||
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz",
|
||||
"integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.7",
|
||||
"@vitest/utils": "4.1.7",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -1984,9 +1966,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
|
||||
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz",
|
||||
"integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -1994,13 +1976,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
|
||||
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz",
|
||||
"integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.7",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
@@ -2087,56 +2069,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/apache-arrow": {
|
||||
"version": "21.1.0",
|
||||
"resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.1.0.tgz",
|
||||
"integrity": "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.11",
|
||||
"@types/command-line-args": "^5.2.3",
|
||||
"@types/command-line-usage": "^5.0.4",
|
||||
"@types/node": "^24.0.3",
|
||||
"command-line-args": "^6.0.1",
|
||||
"command-line-usage": "^7.0.1",
|
||||
"flatbuffers": "^25.1.24",
|
||||
"json-bignum": "^0.0.3",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"bin": {
|
||||
"arrow2csv": "bin/arrow2csv.js"
|
||||
}
|
||||
},
|
||||
"node_modules/apache-arrow/node_modules/@types/node": {
|
||||
"version": "24.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.0.tgz",
|
||||
"integrity": "sha512-5vtOqGQr4NJKeEzV441FcOi2MeG9UTWq9LqVLGneDdu4vlX17H8kQ2PA2UmNwCUGPVDj4oBjNhS7ReVEIWJJrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apache-arrow/node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-back": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
|
||||
"integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -2261,37 +2199,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk-template": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
|
||||
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
@@ -2374,44 +2281,6 @@
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.2.tgz",
|
||||
"integrity": "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^6.2.3",
|
||||
"find-replace": "^5.0.2",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@75lb/nature": "latest"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@75lb/nature": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz",
|
||||
"integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^6.2.2",
|
||||
"chalk-template": "^0.4.0",
|
||||
"table-layout": "^4.1.1",
|
||||
"typical": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
@@ -2950,23 +2819,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/find-replace": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz",
|
||||
"integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@75lb/nature": "latest"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@75lb/nature": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/flatbuffers": {
|
||||
"version": "25.9.23",
|
||||
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
|
||||
@@ -3205,6 +3057,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3253,9 +3106,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.23",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
|
||||
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
|
||||
"version": "4.12.18",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
|
||||
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -3443,14 +3296,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bignum": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
|
||||
"integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -3742,12 +3587,6 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
@@ -4452,13 +4291,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
|
||||
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@oxc-project/types": "=0.132.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4468,21 +4307,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
"@rolldown/binding-android-arm64": "1.0.2",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.2",
|
||||
"@rolldown/binding-darwin-x64": "1.0.2",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.2",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.2",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.2",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.2",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.2",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.2",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
@@ -4854,6 +4693,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@@ -4862,19 +4702,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/table-layout": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
|
||||
"integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^6.2.2",
|
||||
"wordwrapjs": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
|
||||
@@ -4921,9 +4748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4967,6 +4794,25 @@
|
||||
"node-gyp-build": "^4.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-c": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.21.4.tgz",
|
||||
"integrity": "sha512-IahxFIhXiY15SUlrt2upBiKSBGdOaE1fjKLK1Ik5zxqGHf6T1rvr3IJrovbsE5sXhypx7Hnmf50gshsppaIihA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tree-sitter": "^0.21.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tree_sitter": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-c-sharp": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz",
|
||||
@@ -5062,6 +4908,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-kotlin": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz",
|
||||
"integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0",
|
||||
"node-gyp-build": "^4.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tree-sitter": "^0.21.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tree_sitter": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-kotlin/node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tree-sitter-php": {
|
||||
"version": "0.23.12",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz",
|
||||
@@ -5162,7 +5035,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
@@ -5240,15 +5114,6 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typical": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
|
||||
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
@@ -5302,17 +5167,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"version": "8.0.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
|
||||
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
"rolldown": "1.0.2",
|
||||
"tinyglobby": "^0.2.16"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -5380,19 +5245,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
|
||||
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz",
|
||||
"integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.8",
|
||||
"@vitest/mocker": "4.1.8",
|
||||
"@vitest/pretty-format": "4.1.8",
|
||||
"@vitest/runner": "4.1.8",
|
||||
"@vitest/snapshot": "4.1.8",
|
||||
"@vitest/spy": "4.1.8",
|
||||
"@vitest/utils": "4.1.8",
|
||||
"@vitest/expect": "4.1.7",
|
||||
"@vitest/mocker": "4.1.7",
|
||||
"@vitest/pretty-format": "4.1.7",
|
||||
"@vitest/runner": "4.1.7",
|
||||
"@vitest/snapshot": "4.1.7",
|
||||
"@vitest/spy": "4.1.7",
|
||||
"@vitest/utils": "4.1.7",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -5420,12 +5285,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.8",
|
||||
"@vitest/browser-preview": "4.1.8",
|
||||
"@vitest/browser-webdriverio": "4.1.8",
|
||||
"@vitest/coverage-istanbul": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/ui": "4.1.8",
|
||||
"@vitest/browser-playwright": "4.1.7",
|
||||
"@vitest/browser-preview": "4.1.7",
|
||||
"@vitest/browser-webdriverio": "4.1.7",
|
||||
"@vitest/coverage-istanbul": "4.1.7",
|
||||
"@vitest/coverage-v8": "4.1.7",
|
||||
"@vitest/ui": "4.1.7",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -5501,15 +5366,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrapjs": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz",
|
||||
"integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
||||
+12
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.7",
|
||||
"version": "1.6.6-rc.132",
|
||||
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
||||
"author": "Abhigyan Patwari",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
@@ -48,15 +48,15 @@
|
||||
"test:integration": "vitest run test/integration",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:parity": "tsx scripts/run-parity.ts",
|
||||
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
|
||||
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-grammars.cjs",
|
||||
"assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs",
|
||||
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs",
|
||||
"prepare": "node scripts/build.js",
|
||||
"prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js"
|
||||
"prepack": "node scripts/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^4.1.0",
|
||||
"@ladybugdb/core": "^0.17.0",
|
||||
"@ladybugdb/core": "^0.16.1",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -71,15 +71,14 @@
|
||||
"ignore": "^7.0.5",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lru-cache": "^11.0.0",
|
||||
"mnemonist": "^0.40.3",
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"onnxruntime-common": "^1.26.0",
|
||||
"onnxruntime-node": "^1.24.0",
|
||||
"pandemonium": "^2.4.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"tree-sitter": "0.21.1",
|
||||
"tree-sitter-c": "0.21.4",
|
||||
"tree-sitter-c-sharp": "0.23.1",
|
||||
"tree-sitter-cpp": "0.23.2",
|
||||
"tree-sitter-go": "^0.23.0",
|
||||
@@ -92,6 +91,11 @@
|
||||
"tree-sitter-typescript": "^0.23.2",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"tree-sitter-kotlin": "^0.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Publish guard: every vendored tree-sitter grammar must ship a loadable binding.
|
||||
*
|
||||
* The npm tarball includes gitnexus/vendor/ (package.json `files`). A grammar is
|
||||
* "covered" on a platform-arch tuple if EITHER a prebuild ships for it OR the
|
||||
* grammar's full source-build set ships (so the install can source-build it,
|
||||
* toolchain permitting). A future lean publish — dropping the ~50 MB of generated
|
||||
* source to ship prebuilds only — is safe ONLY once every grammar has all six
|
||||
* prebuilds; doing it while any grammar still lacks a prebuild would ship a
|
||||
* grammar with NO loadable binding (neither prebuild nor buildable source) → that
|
||||
* language is silently dead for users.
|
||||
*
|
||||
* HOW SOURCE INCLUSION IS DECIDED. The `files` allow-list OVERRIDES `.npmignore`
|
||||
* for the vendored subtree (verified: an active "vendor/(star-star)/src/parser.c"
|
||||
* in .npmignore does NOT drop it from `npm pack`). So `.npmignore` can never
|
||||
* exclude vendored source — the ONLY lever is the `files` field. A broad `vendor`
|
||||
* ships the whole subtree (source + prebuilds); a lean publish narrows `files` to
|
||||
* non-source subpaths. This guard therefore reads `files` directly rather than
|
||||
* shelling out to `npm pack` (which, in prepack, would re-enter this guard and,
|
||||
* on npm versions that don't honor --ignore-scripts for prepare/prepack, run the
|
||||
* full build — slow enough to time out and fragile).
|
||||
*
|
||||
* Wired via `prepack`, so it fails `npm pack` / `npm publish` if the invariant is
|
||||
* violated.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TUPLES = [
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'darwin-x64',
|
||||
'darwin-arm64',
|
||||
'win32-x64',
|
||||
'win32-arm64',
|
||||
];
|
||||
|
||||
// Source-build inputs (relative to vendor/<name>/) whose presence makes a grammar
|
||||
// source-buildable. Per-grammar we only require the ones that exist on disk (e.g.
|
||||
// tree-sitter-c has no external scanner.c).
|
||||
const SOURCE_BUILD_REL = [
|
||||
'binding.gyp',
|
||||
'bindings/node/binding.cc',
|
||||
'src/parser.c',
|
||||
'src/scanner.c',
|
||||
'src/tree_sitter/parser.h',
|
||||
];
|
||||
|
||||
/**
|
||||
* Does the package.json `files` allow-list ship the WHOLE vendor subtree (and
|
||||
* therefore the vendored grammar source)? A bare `vendor` (optionally with a
|
||||
* trailing slash or `/**`/`/*`) includes everything under vendor/. A lean publish
|
||||
* replaces that with non-source subpaths, so this returns false and grammars must
|
||||
* then rely on prebuilds.
|
||||
*/
|
||||
function filesShipsVendorSource(filesField) {
|
||||
return (filesField || []).some((f) => {
|
||||
const n = String(f)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/+$/, '')
|
||||
.replace(/\/\*\*?$/, '');
|
||||
return n === 'vendor';
|
||||
});
|
||||
}
|
||||
|
||||
/** The on-disk source-build inputs for a grammar (relative paths). */
|
||||
function sourceBuildSet(grammarDir) {
|
||||
return SOURCE_BUILD_REL.filter((rel) => fs.existsSync(path.join(grammarDir, rel)));
|
||||
}
|
||||
|
||||
/** True when a grammar can be source-built from its vendored files (has gyp + parser). */
|
||||
function isBuildableFromSource(grammarDir) {
|
||||
const set = sourceBuildSet(grammarDir);
|
||||
return set.includes('binding.gyp') && set.includes('src/parser.c');
|
||||
}
|
||||
|
||||
/** Count platform-arch tuples with a committed prebuilt .node on disk. */
|
||||
function countPrebuiltTuples(grammarDir) {
|
||||
const pdir = path.join(grammarDir, 'prebuilds');
|
||||
let n = 0;
|
||||
for (const t of TUPLES) {
|
||||
const td = path.join(pdir, t);
|
||||
try {
|
||||
if (fs.statSync(td).isDirectory() && fs.readdirSync(td).some((f) => f.endsWith('.node'))) {
|
||||
n++;
|
||||
}
|
||||
} catch {
|
||||
/* tuple dir absent — not covered */
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure core (exported for tests). `grammars` is a list of
|
||||
* `{ name, prebuilt: 0..6, shipsSource: boolean }`. Returns human-readable
|
||||
* problem strings; an empty array means the pack is publish-safe.
|
||||
*/
|
||||
function findCoverageProblems({ grammars }) {
|
||||
const problems = [];
|
||||
for (const g of grammars) {
|
||||
if (g.prebuilt < 6 && !g.shipsSource) {
|
||||
const missing = 6 - g.prebuilt;
|
||||
problems.push(
|
||||
`${g.name}: ${g.prebuilt}/6 prebuilds and its vendored source is not shipped ` +
|
||||
`(the package.json \`files\` field excludes it, or it is not buildable) — would ship ` +
|
||||
`with no loadable binding on ${missing} platform-arch tuple(s).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
function collectGrammars(vendorDir, shipsVendorSource) {
|
||||
if (!fs.existsSync(vendorDir)) return [];
|
||||
return fs
|
||||
.readdirSync(vendorDir)
|
||||
.filter((d) => /^tree-sitter-/.test(d))
|
||||
.map((name) => {
|
||||
const dir = path.join(vendorDir, name);
|
||||
return {
|
||||
name,
|
||||
prebuilt: countPrebuiltTuples(dir),
|
||||
// Source ships when `files` includes the vendor subtree AND the grammar
|
||||
// actually carries a buildable source set on disk.
|
||||
shipsSource: shipsVendorSource && isBuildableFromSource(dir),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
const gitnexusRoot = path.join(__dirname, '..');
|
||||
const vendorDir = path.join(gitnexusRoot, 'vendor');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'));
|
||||
const shipsVendorSource = filesShipsVendorSource(pkg.files);
|
||||
|
||||
const grammars = collectGrammars(vendorDir, shipsVendorSource);
|
||||
if (grammars.length === 0) {
|
||||
console.error(`[publish-guard] No vendored tree-sitter grammars found under ${vendorDir}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const problems = findCoverageProblems({ grammars });
|
||||
if (problems.length > 0) {
|
||||
console.error('[publish-guard] Refusing to publish — a vendored grammar would ship unusable:');
|
||||
for (const p of problems) console.error(` - ${p}`);
|
||||
console.error(
|
||||
'\nFix: either commit the missing prebuilds (run the build-tree-sitter-prebuilds\n' +
|
||||
'workflow) or keep the vendored source in the package.json `files` field.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceShippers = grammars.filter((g) => g.shipsSource).length;
|
||||
console.log(
|
||||
`[publish-guard] OK — ${grammars.length} vendored grammar(s) covered ` +
|
||||
`(${sourceShippers} shipping source, ${grammars.length - sourceShippers} prebuilds-only).`,
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = {
|
||||
findCoverageProblems,
|
||||
filesShipsVendorSource,
|
||||
isBuildableFromSource,
|
||||
sourceBuildSet,
|
||||
countPrebuiltTuples,
|
||||
collectGrammars,
|
||||
TUPLES,
|
||||
SOURCE_BUILD_REL,
|
||||
};
|
||||
@@ -4,8 +4,10 @@
|
||||
* isolating the resolution cost from parse / heritage / pipeline
|
||||
* overhead.
|
||||
*
|
||||
* Usage: npx tsx scripts/bench-scope-resolution.ts
|
||||
* Usage: REGISTRY_PRIMARY_PYTHON=1 npx tsx scripts/bench-scope-resolution.ts
|
||||
*/
|
||||
process.env.REGISTRY_PRIMARY_PYTHON = '1';
|
||||
|
||||
import { generateId } from '../src/lib/utils.js';
|
||||
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
|
||||
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// FTS evict→reload RSS repro (gitnexus-enterprise PR #222 / local U3).
|
||||
//
|
||||
// Settles ONE empirical question that no static read can answer: when a
|
||||
// LadybugDB database that has `LOAD EXTENSION fts` applied is closed and a
|
||||
// fresh one is opened + re-LOADed (the pool's evict→reload cycle), does the
|
||||
// native FTS arena get reclaimed by `db.close()` — or is it stranded, so RSS
|
||||
// climbs without bound over a long-lived MCP `serve` session?
|
||||
//
|
||||
// • PLATEAU across cycles → db.close() reclaims the FTS arena; the OSS pool's
|
||||
// footprint is bounded by MAX_POOL_SIZE (~5 live arenas). No unbounded leak;
|
||||
// the #222 worker-isolation rewrite (plan U4) is NOT justified for OSS.
|
||||
// • MONOTONIC CLIMB → the FTS arena is stranded per reopen; the user's
|
||||
// hypothesis holds and U4 (route FTS reads through a reclaimable worker) is
|
||||
// justified.
|
||||
//
|
||||
// SCOPE OF THE VERDICT (read before citing it). A per-reload FTS-arena leak
|
||||
// would be PROPORTIONAL to the index size. A small fixture therefore produces a
|
||||
// small per-cycle increment that an absolute threshold can read as PLATEAU even
|
||||
// when a production-scale graph would leak visibly. So:
|
||||
// - `--rows` controls fixture size; run it LARGE (tens of thousands) before
|
||||
// concluding "no leak". The default is deliberately not tiny.
|
||||
// - The verdict (in fts-rss-verdict.mjs) keys on slope DECELERATION, not total
|
||||
// delta, with a noise floor that scales with the working-set growth
|
||||
// (peak−baseline) so sensitivity tracks fixture/arena size — NOT the pre-DB
|
||||
// baseline RSS. A sustained sub-floor positive slope is INCONCLUSIVE (a slow
|
||||
// creep RSS can't distinguish from noise), never a clean PLATEAU.
|
||||
// - The PLATEAU verdict is only valid for the corpus size it was run at; the
|
||||
// output states that size. The production-faithful confirmation is a
|
||||
// `--via-pool` run against a real large analyzed repo over a long session.
|
||||
//
|
||||
// Two modes:
|
||||
// (default) NATIVE — reproduces the native sequence doInitLbug()+closeOne()
|
||||
// perform (open Database → new Connection → LOAD EXTENSION fts →
|
||||
// QUERY_FTS_INDEX → close), against K self-built FTS fixtures, with no
|
||||
// gitnexus build required. `--no-await-close` mirrors the pool's
|
||||
// fire-and-forget close instead of awaiting (the production close shape).
|
||||
// --via-pool <lbugPath> — drives the REAL gitnexus pool from compiled dist
|
||||
// (initLbug → executeParameterized → closeLbug) against an existing analyzed
|
||||
// repo, exercising the production path + the GITNEXUS_POOL_RSS_TRACE
|
||||
// instrumentation. Probes ALL FTS indexes the repo has. Forces an explicit
|
||||
// close+reinit each cycle. Run `node scripts/build.js` first so the dist
|
||||
// reflects the current pool-adapter (incl. the RSS trace).
|
||||
//
|
||||
// Run with --expose-gc so RSS excludes V8-heap noise:
|
||||
// node --expose-gc gitnexus/scripts/bench/fts-evict-reload-rss.mjs
|
||||
// node --expose-gc gitnexus/scripts/bench/fts-evict-reload-rss.mjs --rows 40000 --cycles 30
|
||||
// GITNEXUS_POOL_RSS_TRACE=1 node --expose-gc \
|
||||
// gitnexus/scripts/bench/fts-evict-reload-rss.mjs --via-pool /path/to/repo/.gitnexus/lbug
|
||||
//
|
||||
// Flags by mode: --rows/--repos/--read-write/--no-await-close apply to NATIVE
|
||||
// only; --cycles applies to both. VIA-POOL warns when a NATIVE-only flag is set.
|
||||
//
|
||||
// Memory benches are noisy. Default is 24 cycles; trust the TREND (slope /
|
||||
// first-third vs last-third), never a single delta. A flat trend at a LARGE
|
||||
// fixture is a real NEGATIVE result (no unbounded leak), not a failed run.
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
// Pure verdict classifier (median, slopeMbPerCycle, classifyVerdict) lives in a
|
||||
// side-effect-free sibling module so it is unit-testable without loading the
|
||||
// native addon or running this bench. See fts-rss-verdict.mjs.
|
||||
import { classifyVerdict, median, slopeMbPerCycle } from './fts-rss-verdict.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const lbugModule = require('@ladybugdb/core');
|
||||
const lbug = lbugModule.default ?? lbugModule;
|
||||
|
||||
const LBUG_MAX_DB_SIZE = 16 * 1024 * 1024 * 1024;
|
||||
|
||||
// ── args ──────────────────────────────────────────────────────────────────
|
||||
function argVal(flag, dflt) {
|
||||
const i = process.argv.indexOf(flag);
|
||||
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : dflt;
|
||||
}
|
||||
const CYCLES = Math.max(6, parseInt(argVal('--cycles', '24'), 10) || 24);
|
||||
const REPOS = Math.max(1, parseInt(argVal('--repos', '6'), 10) || 6); // >5 mirrors LRU thrash
|
||||
// Fixture size. Default is large enough that a size-proportional leak would be
|
||||
// visible across cycles; raise it further before trusting a PLATEAU verdict.
|
||||
const ROWS = Math.max(100, parseInt(argVal('--rows', '8000'), 10) || 8000);
|
||||
const VIA_POOL = argVal('--via-pool', null);
|
||||
const READONLY = !process.argv.includes('--read-write');
|
||||
const AWAIT_CLOSE = !process.argv.includes('--no-await-close');
|
||||
|
||||
if (VIA_POOL) {
|
||||
// These flags are consumed only by NATIVE mode; warn rather than ignore
|
||||
// silently so a VIA-POOL run is not misread as honoring them.
|
||||
const ignored = ['--rows', '--repos', '--read-write', '--no-await-close'].filter((f) =>
|
||||
process.argv.includes(f),
|
||||
);
|
||||
if (ignored.length) {
|
||||
console.error(
|
||||
`[fts-rss] NOTE: ${ignored.join(', ')} apply to NATIVE mode only; ignored in --via-pool.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof global.gc !== 'function') {
|
||||
console.error(
|
||||
'[fts-rss] WARNING: run with --expose-gc for clean RSS samples ' +
|
||||
'(`node --expose-gc <thisfile>`). Continuing without forced GC — results are noisier.',
|
||||
);
|
||||
}
|
||||
|
||||
const gc = () => {
|
||||
if (typeof global.gc === 'function') {
|
||||
global.gc();
|
||||
global.gc();
|
||||
}
|
||||
};
|
||||
const rssMb = () => Math.round(process.memoryUsage().rss / (1024 * 1024));
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// ── fixture: a minimal FTS-bearing .lbug ────────────────────────────────────
|
||||
const WORDS = [
|
||||
'login auth session token user password validate verify credential',
|
||||
'parse tree syntax node grammar lexer token ast traversal visitor',
|
||||
'graph query cypher match relation node edge pattern aggregate index',
|
||||
'memory pool buffer arena allocate reclaim evict cache resident heap',
|
||||
'search rank score bm25 fts index stem porter keyword document corpus',
|
||||
'worker fork process spawn kill reclaim isolate native binding addon',
|
||||
];
|
||||
|
||||
function buildFixture(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const dbPath = path.join(dir, 'fixture.lbug');
|
||||
const db = new lbug.Database(dbPath, 0, false, false, LBUG_MAX_DB_SIZE);
|
||||
const conn = new lbug.Connection(db);
|
||||
return (async () => {
|
||||
await conn.query('LOAD EXTENSION fts');
|
||||
await conn.query(
|
||||
'CREATE NODE TABLE Doc(id STRING, name STRING, content STRING, PRIMARY KEY(id))',
|
||||
);
|
||||
// Batch-insert via UNWIND so large fixtures (`--rows`) build in seconds
|
||||
// instead of one round-trip per row. The fixture size drives the per-arena
|
||||
// FTS allocation, which is what makes a size-proportional leak observable.
|
||||
const rows = [];
|
||||
for (let i = 0; i < ROWS; i++) {
|
||||
const w = WORDS[i % WORDS.length];
|
||||
const name = `sym_${i}`;
|
||||
const content = `${w} ${name} block number ${i} ${WORDS[(i + 3) % WORDS.length]}`;
|
||||
rows.push({ id: `doc:${i}`, name, content });
|
||||
}
|
||||
const INSERT_CHUNK = 2000;
|
||||
for (let i = 0; i < rows.length; i += INSERT_CHUNK) {
|
||||
const chunk = rows.slice(i, i + INSERT_CHUNK);
|
||||
const stmt = await conn.prepare(
|
||||
'UNWIND $rows AS r CREATE (:Doc {id: r.id, name: r.name, content: r.content})',
|
||||
);
|
||||
await conn.execute(stmt, { rows: chunk });
|
||||
}
|
||||
await conn.query(
|
||||
"CALL CREATE_FTS_INDEX('Doc', 'doc_fts', ['name', 'content'], stemmer := 'porter')",
|
||||
);
|
||||
await conn.close();
|
||||
await db.close();
|
||||
return dbPath;
|
||||
})();
|
||||
}
|
||||
|
||||
const QUERIES = ['login token', 'parse node', 'memory arena', 'search index', 'worker reclaim'];
|
||||
|
||||
// ── NATIVE mode ─────────────────────────────────────────────────────────────
|
||||
async function runNative() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fts-rss-'));
|
||||
console.error(
|
||||
`[fts-rss] NATIVE: ${REPOS} fixtures × ${ROWS} rows × ${CYCLES} cycles ` +
|
||||
`(readOnly=${READONLY}, awaitClose=${AWAIT_CLOSE})`,
|
||||
);
|
||||
console.error(`[fts-rss] building ${REPOS} FTS fixture(s) under ${root} …`);
|
||||
|
||||
const srcDb = await buildFixture(path.join(root, 'src'));
|
||||
const repoPaths = [];
|
||||
for (let k = 0; k < REPOS; k++) {
|
||||
const dst = path.join(root, `repo-${k}`);
|
||||
fs.cpSync(path.dirname(srcDb), dst, { recursive: true });
|
||||
repoPaths.push(path.join(dst, 'fixture.lbug'));
|
||||
}
|
||||
|
||||
// Mirror the pool's evict→reload: each visit opens a FRESH Database, makes a
|
||||
// Connection, LOADs fts, runs an FTS query, then closes — no caching, so every
|
||||
// visit is a reload. K>5 amplifies the LRU-thrash signal the pool would see.
|
||||
const series = [];
|
||||
gc();
|
||||
await sleep(50);
|
||||
const baseline = rssMb();
|
||||
console.error(`[fts-rss] baseline RSS=${baseline}MB`);
|
||||
|
||||
for (let cycle = 0; cycle < CYCLES; cycle++) {
|
||||
for (let k = 0; k < REPOS; k++) {
|
||||
const db = new lbug.Database(repoPaths[k], 0, false, READONLY, LBUG_MAX_DB_SIZE);
|
||||
const conn = new lbug.Connection(db);
|
||||
try {
|
||||
await conn.query('LOAD EXTENSION fts'); // the per-reload re-LOAD under test
|
||||
const q = QUERIES[(cycle + k) % QUERIES.length];
|
||||
const res = await conn.query(
|
||||
`CALL QUERY_FTS_INDEX('Doc', 'doc_fts', '${q}') RETURN node.id AS id, score ORDER BY score DESC LIMIT 20`,
|
||||
);
|
||||
// Drain so the query actually materializes results.
|
||||
if (res && typeof res.getAll === 'function') await res.getAll();
|
||||
} catch (e) {
|
||||
console.error(`[fts-rss] query error (cycle ${cycle}, repo ${k}): ${e?.message || e}`);
|
||||
} finally {
|
||||
// AWAIT_CLOSE (default) is the best case for reclamation. --no-await-close
|
||||
// mirrors the pool's fire-and-forget close (closeOne: db.close().catch())
|
||||
// so a leak that only manifests without awaiting is not hidden.
|
||||
if (AWAIT_CLOSE) {
|
||||
try {
|
||||
await conn.close();
|
||||
await db.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} else {
|
||||
conn.close().catch(() => {});
|
||||
db.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
gc();
|
||||
// Longer settle when not awaiting close, so fire-and-forget native teardown
|
||||
// has a chance to complete before the RSS sample (avoids a false PLATEAU).
|
||||
await sleep(AWAIT_CLOSE ? 20 : 200);
|
||||
const rss = rssMb();
|
||||
series.push(rss);
|
||||
console.error(`[fts-rss] cycle ${String(cycle + 1).padStart(3)}/${CYCLES} rssMB=${rss}`);
|
||||
}
|
||||
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
return { baseline, series, corpus: `${REPOS}×${ROWS} rows, native, awaitClose=${AWAIT_CLOSE}` };
|
||||
}
|
||||
|
||||
// ── VIA-POOL mode (real gitnexus pool from compiled dist) ───────────────────
|
||||
async function runViaPool(lbugPath) {
|
||||
if (!fs.existsSync(lbugPath)) {
|
||||
console.error(`[fts-rss] --via-pool path not found: ${lbugPath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
// Compiled dist is required (the pool pulls the native addon + many modules).
|
||||
const distUrl = new URL('../../dist/core/lbug/pool-adapter.js', import.meta.url);
|
||||
let pool;
|
||||
try {
|
||||
pool = await import(distUrl.href);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[fts-rss] could not import compiled pool-adapter (${e?.message}). ` +
|
||||
`Run \`node scripts/build.js\` first, or use NATIVE mode.`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const { initLbug, executeParameterized, closeLbug } = pool;
|
||||
console.error(
|
||||
`[fts-rss] VIA-POOL on ${lbugPath} × ${CYCLES} cycles ` +
|
||||
`(explicit closeLbug+initLbug per cycle = forced evict→reload)`,
|
||||
);
|
||||
|
||||
// Probe ALL FTS indexes the analyzed graph carries (mirrors fts-schema.ts
|
||||
// FTS_INDEXES) so the per-cycle FTS arena load matches production, not a
|
||||
// 2-of-5 subset that would understate it.
|
||||
const FTS_INDEXES = [
|
||||
{ table: 'File', indexName: 'file_fts' },
|
||||
{ table: 'Function', indexName: 'function_fts' },
|
||||
{ table: 'Class', indexName: 'class_fts' },
|
||||
{ table: 'Method', indexName: 'method_fts' },
|
||||
{ table: 'Interface', indexName: 'interface_fts' },
|
||||
];
|
||||
|
||||
const series = [];
|
||||
gc();
|
||||
const baseline = rssMb();
|
||||
console.error(`[fts-rss] baseline RSS=${baseline}MB`);
|
||||
|
||||
for (let cycle = 0; cycle < CYCLES; cycle++) {
|
||||
try {
|
||||
await initLbug(lbugPath, lbugPath);
|
||||
const q = QUERIES[cycle % QUERIES.length];
|
||||
for (const { table, indexName } of FTS_INDEXES) {
|
||||
await executeParameterized(
|
||||
lbugPath,
|
||||
`CALL QUERY_FTS_INDEX('${table}', '${indexName}', $q) RETURN node.id AS id, score ORDER BY score DESC LIMIT 20`,
|
||||
{ q },
|
||||
).catch(() => []); // index may not exist for this graph — that's fine
|
||||
}
|
||||
await closeLbug(lbugPath); // force eviction → next cycle reopens + re-LOADs fts
|
||||
} catch (e) {
|
||||
console.error(`[fts-rss] pool cycle ${cycle} error: ${e?.message || e}`);
|
||||
}
|
||||
gc();
|
||||
// closeLbug fires a fire-and-forget native close (pool closeOne:
|
||||
// db.close().catch()), so settle longer than NATIVE's awaited close to let
|
||||
// native teardown finish before sampling — else a real leak reads PLATEAU.
|
||||
await sleep(200);
|
||||
const rss = rssMb();
|
||||
series.push(rss);
|
||||
console.error(`[fts-rss] cycle ${String(cycle + 1).padStart(3)}/${CYCLES} rssMB=${rss}`);
|
||||
}
|
||||
await closeLbug().catch(() => {});
|
||||
return { baseline, series, corpus: `via-pool ${path.basename(path.dirname(lbugPath))}` };
|
||||
}
|
||||
|
||||
// ── verdict ─────────────────────────────────────────────────────────────────
|
||||
function verdict({ baseline, series, corpus }) {
|
||||
const third = Math.max(1, Math.floor(series.length / 3));
|
||||
const firstMed = median(series.slice(0, third));
|
||||
const lastMed = median(series.slice(-third));
|
||||
const delta = lastMed - firstMed;
|
||||
const slope = slopeMbPerCycle(series);
|
||||
|
||||
// All label logic lives in the pure, unit-tested classifier (fts-rss-verdict.mjs):
|
||||
// epsilon-first flat→PLATEAU, decelerated→PLATEAU, sustained-sub-floor→INCONCLUSIVE,
|
||||
// ≥floor sustained→CLIMB, step→INCONCLUSIVE; floor scales with the working-set
|
||||
// growth (peak−baseline), not the pre-DB baseline RSS.
|
||||
const {
|
||||
verdict: label,
|
||||
firstHalfSlope,
|
||||
secondHalfSlope,
|
||||
decelRatio,
|
||||
floor,
|
||||
stepDiscontinuity,
|
||||
maxJump,
|
||||
peak,
|
||||
} = classifyVerdict(series, baseline);
|
||||
|
||||
console.log('\n==================== FTS evict→reload RSS verdict ====================');
|
||||
console.log(`corpus: ${corpus}`);
|
||||
console.log(`samples (MB): ${series.join(' ')}`);
|
||||
console.log(
|
||||
`baseline=${baseline} firstThirdMed=${firstMed} lastThirdMed=${lastMed} delta=${delta}MB ` +
|
||||
`peak=${peak} overallSlope=${slope.toFixed(2)} firstHalfSlope=${firstHalfSlope.toFixed(2)} ` +
|
||||
`secondHalfSlope=${secondHalfSlope.toFixed(2)}MB/cycle floor=${floor.toFixed(2)} decelRatio=${decelRatio.toFixed(2)} ` +
|
||||
`maxJump=${maxJump}MB step=${stepDiscontinuity} cycles=${series.length}`,
|
||||
);
|
||||
if (label === 'CLIMB') {
|
||||
console.log(
|
||||
'VERDICT: CLIMB — the per-cycle increment is SUSTAINED (second-half slope ≈ first-half),\n' +
|
||||
' i.e. RSS rises ~linearly with no decay. The native FTS arena is NOT reclaimed\n' +
|
||||
' by db.close(); the leak is real over a long-lived session.\n' +
|
||||
' → plan U4 (worker/process isolation of the FTS read path) is JUSTIFIED.',
|
||||
);
|
||||
} else if (label === 'PLATEAU') {
|
||||
console.log(
|
||||
`VERDICT: PLATEAU at this corpus (${corpus}) — the per-cycle increment DECAYS to flat\n` +
|
||||
' (second-half slope below the noise floor). db.close() reclaims the FTS arena;\n' +
|
||||
' footprint is bounded (and the pool further caps it at MAX_POOL_SIZE). No\n' +
|
||||
' unbounded leak. Caveat: synthetic fixture — confirm with a --via-pool run\n' +
|
||||
' against a real large analyzed repo before fully closing plan U4.',
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`VERDICT: INCONCLUSIVE at this corpus (${corpus}) — the run is noisy (step discontinuity)\n` +
|
||||
' or still decelerating without reaching flat, so neither a clean PLATEAU nor a\n' +
|
||||
' sustained linear CLIMB can be asserted. NATIVE synthetic runs do not resolve\n' +
|
||||
' this reliably at scale. The definitive test is a --via-pool run against a real\n' +
|
||||
' large analyzed repo over many cycles (with GITNEXUS_POOL_RSS_TRACE=1). Plan U4\n' +
|
||||
' stays GATED — neither closed nor built on this evidence.',
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`MACHINE: ${JSON.stringify({ mode: VIA_POOL ? 'via-pool' : 'native', corpus, baseline, firstMed, lastMed, delta, overallSlope: Number(slope.toFixed(3)), firstHalfSlope: Number(firstHalfSlope.toFixed(3)), secondHalfSlope: Number(secondHalfSlope.toFixed(3)), floor: Number(floor.toFixed(3)), decelRatio: Number(decelRatio.toFixed(3)), maxJump, stepDiscontinuity, peak, cycles: series.length, verdict: label })}`,
|
||||
);
|
||||
console.log('=====================================================================\n');
|
||||
}
|
||||
|
||||
// ── main ────────────────────────────────────────────────────────────────────
|
||||
(async () => {
|
||||
const result = VIA_POOL ? await runViaPool(VIA_POOL) : await runNative();
|
||||
verdict(result);
|
||||
process.exit(0);
|
||||
})().catch((e) => {
|
||||
console.error('[fts-rss] fatal:', e?.stack || e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
// Pure, side-effect-free verdict classifier for the FTS evict→reload RSS bench
|
||||
// (fts-evict-reload-rss.mjs). Extracted so it can be unit-tested WITHOUT importing
|
||||
// the native LadybugDB addon or running the bench — this module has zero imports
|
||||
// and zero module-scope side effects. Do not add imports or top-level statements.
|
||||
//
|
||||
// The discriminant between a real leak and allocator warmup is SLOPE DECELERATION,
|
||||
// not total delta. A true per-reload leak (stranded FTS arena) rises ~linearly:
|
||||
// the second-half slope stays ≈ the first-half slope. Allocator working-set warmup
|
||||
// rises then flattens: the second-half slope decays to a fraction of the first.
|
||||
//
|
||||
// Thresholds:
|
||||
// EPSILON (~0.1 MB/cycle) — below this the tail is effectively flat (no leak).
|
||||
// SUSTAIN_FLOOR (0.5 MB/cycle) — the base noise floor.
|
||||
// The floor SCALES with the working-set growth (peak − baseline), NOT the pre-DB
|
||||
// `baseline` RSS: baseline is interpreter/addon overhead (and is LARGER in
|
||||
// --via-pool mode), so a baseline-keyed floor would inflate and HIDE leaks. A
|
||||
// bigger fixture has a bigger arena and bigger per-cycle noise, so the floor
|
||||
// rises with the working set: floor = SUSTAIN_FLOOR · max(1, (peak−baseline)/REF).
|
||||
|
||||
export const EPSILON_MB_PER_CYCLE = 0.1;
|
||||
export const SUSTAIN_FLOOR = 0.5;
|
||||
// Reference working-set (MB) at which the floor equals SUSTAIN_FLOOR; the floor
|
||||
// scales up linearly for larger arenas. ~200 MB ≈ a small FTS fixture's footprint.
|
||||
export const FLOOR_REF_WORKINGSET_MB = 200;
|
||||
|
||||
export function median(xs) {
|
||||
const s = [...xs].sort((a, b) => a - b);
|
||||
const m = Math.floor(s.length / 2);
|
||||
return s.length % 2 ? s[m] : Math.round((s[m - 1] + s[m]) / 2);
|
||||
}
|
||||
|
||||
export function slopeMbPerCycle(series) {
|
||||
// Least-squares slope of rss vs cycle index.
|
||||
const n = series.length;
|
||||
if (n < 2) return 0;
|
||||
const xs = series.map((_, i) => i);
|
||||
const xMean = xs.reduce((a, b) => a + b, 0) / n;
|
||||
const yMean = series.reduce((a, b) => a + b, 0) / n;
|
||||
let num = 0;
|
||||
let den = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
num += (xs[i] - xMean) * (series[i] - yMean);
|
||||
den += (xs[i] - xMean) ** 2;
|
||||
}
|
||||
return den === 0 ? 0 : num / den;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an RSS-per-cycle series into PLATEAU / CLIMB / INCONCLUSIVE.
|
||||
* Pure: no I/O, no globals. `baseline` is the pre-DB RSS; `peak` defaults to the
|
||||
* series max. Returns the label plus the diagnostics the bench prints.
|
||||
*/
|
||||
export function classifyVerdict(series, baseline, peak = Math.max(...series)) {
|
||||
const cycles = series.length;
|
||||
const half = Math.max(1, Math.floor(cycles / 2));
|
||||
const firstHalfSlope = slopeMbPerCycle(series.slice(0, half));
|
||||
const secondHalfSlope = slopeMbPerCycle(series.slice(-half));
|
||||
const decelRatio = secondHalfSlope / Math.max(firstHalfSlope, 1e-9);
|
||||
|
||||
// Step discontinuity: a single cycle-to-cycle jump far larger than the typical
|
||||
// per-cycle delta — a one-time allocator/arena reservation (then flat), not a
|
||||
// per-reload leak, but a noisy run we won't claim a clean result on.
|
||||
const deltas = series.slice(1).map((v, i) => v - series[i]);
|
||||
const absDeltas = deltas.map(Math.abs).sort((a, b) => a - b);
|
||||
const medAbsDelta = absDeltas.length ? absDeltas[Math.floor(absDeltas.length / 2)] : 0;
|
||||
const maxJump = deltas.length ? Math.max(...deltas) : 0;
|
||||
const stepDiscontinuity = maxJump > Math.max(30, 5 * Math.max(medAbsDelta, 1));
|
||||
|
||||
// Working-set-scaled floor (see header). Guard against a negative working set.
|
||||
const workingSet = Math.max(0, peak - baseline);
|
||||
const floor = SUSTAIN_FLOOR * Math.max(1, workingSet / FLOOR_REF_WORKINGSET_MB);
|
||||
|
||||
const SUSTAINED = 0.6; // decelRatio at/above which the tail is "not decaying"
|
||||
let verdict;
|
||||
if (stepDiscontinuity) {
|
||||
verdict = 'INCONCLUSIVE';
|
||||
} else if (secondHalfSlope < EPSILON_MB_PER_CYCLE) {
|
||||
// Effectively flat — no leak, regardless of decelRatio (a flat-from-start run
|
||||
// has decelRatio ≈ 1 but is still PLATEAU). This gate is what keeps a true
|
||||
// negative from being over-corrected into INCONCLUSIVE.
|
||||
verdict = 'PLATEAU';
|
||||
} else if (secondHalfSlope >= floor) {
|
||||
// Tail is still substantial: sustained → real leak; decelerating → unresolved.
|
||||
verdict = decelRatio >= SUSTAINED ? 'CLIMB' : 'INCONCLUSIVE';
|
||||
} else if (decelRatio < SUSTAINED) {
|
||||
// Below the floor AND decelerating — warmup converged toward flat → PLATEAU.
|
||||
verdict = 'PLATEAU';
|
||||
} else {
|
||||
// Below the floor but SUSTAINED — a slow steady creep RSS can't distinguish
|
||||
// from noise at this scale. The honest label is "not resolved", NEVER a clean
|
||||
// PLATEAU ("no leak"). This is the headline tri-review fix.
|
||||
verdict = 'INCONCLUSIVE';
|
||||
}
|
||||
|
||||
return {
|
||||
verdict,
|
||||
firstHalfSlope,
|
||||
secondHalfSlope,
|
||||
decelRatio,
|
||||
floor,
|
||||
stepDiscontinuity,
|
||||
maxJump,
|
||||
peak,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build tree-sitter-dart native binding in node_modules/ after materialize-vendor-grammars.cjs.
|
||||
* Vendored source lives in vendor/ only; see #836 and #1728.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Opt-out: skip the native rebuild entirely. Dart parsing becomes
|
||||
// unavailable but `npm install gitnexus` finishes much faster on machines
|
||||
// without a C++ toolchain. Strict `=== '1'` only — '=true', '=yes', '=0'
|
||||
// (read as a string), and any other value all fall through to the rebuild.
|
||||
if (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1') {
|
||||
console.warn(
|
||||
'[tree-sitter-dart] Skipping build (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1). Dart parsing will be unavailable until reinstalled without the env var.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const dartDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-dart');
|
||||
const bindingGyp = path.join(dartDir, 'binding.gyp');
|
||||
const bindingNode = path.join(dartDir, 'build', 'Release', 'tree_sitter_dart_binding.node');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(bindingGyp) || fs.existsSync(bindingNode)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
require.resolve('node-addon-api');
|
||||
require.resolve('node-gyp-build');
|
||||
} catch (resolveErr) {
|
||||
console.warn(
|
||||
'[tree-sitter-dart] Skipping build: hoisted build deps not resolvable (%s).',
|
||||
resolveErr.message,
|
||||
);
|
||||
console.warn(
|
||||
'[tree-sitter-dart] Dart parsing will be unavailable. Install without --no-optional and with scripts enabled to build.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('[tree-sitter-dart] Building native binding...');
|
||||
execSync('npx node-gyp rebuild', {
|
||||
cwd: dartDir,
|
||||
stdio: 'pipe',
|
||||
timeout: 180000,
|
||||
});
|
||||
console.log('[tree-sitter-dart] Native binding built successfully');
|
||||
} catch (err) {
|
||||
console.warn('[tree-sitter-dart] Could not build native binding:', err.message);
|
||||
console.warn(
|
||||
'[tree-sitter-dart] Dart parsing will be unavailable. Non-Dart functionality is unaffected.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Activate the vendored tree-sitter native bindings after
|
||||
* materialize-vendor-grammars.cjs. One registry-driven script replaces the
|
||||
* former per-grammar build-tree-sitter-<name>.cjs files (they were ~95%
|
||||
* identical).
|
||||
*
|
||||
* For each grammar the resolution order is identical:
|
||||
* 1. If the package isn't materialized (no binding.gyp) or the binding is
|
||||
* already built, do nothing.
|
||||
* 2. Prefer a committed prebuild for this platform-arch (toolchain-free) via
|
||||
* node-gyp-build — the goal once build-tree-sitter-prebuilds.yml has
|
||||
* populated all six tuples.
|
||||
* 3. Otherwise source-build from the vendored grammar source (binding.gyp +
|
||||
* src/) so parsing still works on any toolchain host — e.g. CI, before the
|
||||
* prebuilds land.
|
||||
*
|
||||
* HARD INVARIANT: this runs in `gitnexus`'s postinstall, so it MUST NEVER throw
|
||||
* or exit non-zero — a failure for any single grammar must not break the install.
|
||||
*
|
||||
* Opt-out: GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (strict '1') skips the OPTIONAL
|
||||
* grammars only. tree-sitter-c is REQUIRED (it backstops upstream's 4/6 ARM
|
||||
* prebuild gap, #2116) and is always built.
|
||||
*
|
||||
* Usage:
|
||||
* node build-tree-sitter-grammars.cjs # all grammars (postinstall)
|
||||
* node build-tree-sitter-grammars.cjs swift c # only the named grammars
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Registry. `display`/`ext` drive the human-readable warnings; `required`
|
||||
// grammars ignore the opt-out gate. Insertion order == build order (c first).
|
||||
const GRAMMARS = {
|
||||
c: { required: true, display: 'C', ext: '.c' },
|
||||
dart: { required: false, display: 'Dart', ext: '.dart' },
|
||||
proto: { required: false, display: 'Proto', ext: '.proto' },
|
||||
swift: { required: false, display: 'Swift', ext: '.swift' },
|
||||
kotlin: { required: false, display: 'Kotlin', ext: '.kt/.kts' },
|
||||
};
|
||||
|
||||
const skipOptional = process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1';
|
||||
|
||||
function buildGrammar(short) {
|
||||
const cfg = GRAMMARS[short];
|
||||
const tag = `[tree-sitter-${short}]`;
|
||||
|
||||
if (!cfg.required && skipOptional) {
|
||||
console.warn(
|
||||
`${tag} Skipping build (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1). ${cfg.display} parsing will be unavailable until reinstalled without the env var.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = path.join(__dirname, '..', 'node_modules', `tree-sitter-${short}`);
|
||||
const bindingGyp = path.join(dir, 'binding.gyp');
|
||||
const bindingNode = path.join(dir, 'build', 'Release', `tree_sitter_${short}_binding.node`);
|
||||
|
||||
try {
|
||||
// Not materialized (no source), or already built — nothing to do.
|
||||
if (!fs.existsSync(bindingGyp) || fs.existsSync(bindingNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer a committed prebuild for this platform-arch (no toolchain needed).
|
||||
try {
|
||||
require('node-gyp-build').path(dir);
|
||||
return;
|
||||
} catch {
|
||||
// No matching prebuild — fall through to the source build below.
|
||||
}
|
||||
|
||||
// The hoisted build deps must be resolvable to source-build.
|
||||
try {
|
||||
require.resolve('node-addon-api');
|
||||
require.resolve('node-gyp-build');
|
||||
} catch (resolveErr) {
|
||||
console.warn(
|
||||
`${tag} Skipping build: hoisted build deps not resolvable (${resolveErr.message}).`,
|
||||
);
|
||||
console.warn(
|
||||
`${tag} ${cfg.display} parsing will be unavailable until a prebuild or toolchain is present.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${tag} No prebuild for this platform — building native binding from source...`);
|
||||
execSync('npx node-gyp rebuild', { cwd: dir, stdio: 'pipe', timeout: 180000 });
|
||||
console.log(`${tag} Native binding built successfully`);
|
||||
} catch (err) {
|
||||
console.warn(`${tag} Could not build native binding:`, err.message);
|
||||
console.warn(
|
||||
`${tag} ${cfg.display} (${cfg.ext}) parsing will be unavailable. Non-${cfg.display} functionality is unaffected.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2).filter(Boolean);
|
||||
const targets = args.length > 0 ? args : Object.keys(GRAMMARS);
|
||||
for (const short of targets) {
|
||||
if (!GRAMMARS[short]) {
|
||||
console.warn(`[tree-sitter] Unknown grammar '${short}' — skipping.`);
|
||||
continue;
|
||||
}
|
||||
// Defensive: never let an unexpected throw escape and fail the install.
|
||||
try {
|
||||
buildGrammar(short);
|
||||
} catch (err) {
|
||||
console.warn(`[tree-sitter-${short}] Unexpected build error (ignored): ${err.message}`);
|
||||
}
|
||||
}
|
||||
// Hard guarantee: postinstall must never exit non-zero.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = { GRAMMARS, buildGrammar };
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build tree-sitter-proto native binding.
|
||||
*
|
||||
* Why this script exists:
|
||||
* tree-sitter-proto is vendored under gitnexus/vendor/tree-sitter-proto/
|
||||
* and copied into node_modules/ by materialize-vendor-grammars.cjs. Previously, the vendored
|
||||
* package had its own `dependencies` and `install` script, which caused
|
||||
* npm to create `vendor/tree-sitter-proto/node_modules/` and
|
||||
* `vendor/tree-sitter-proto/build/` during install. Those directories
|
||||
* blocked `rmdir` on global-install upgrade, producing:
|
||||
*
|
||||
* ENOTEMPTY: directory not empty, rmdir
|
||||
* '.../gitnexus/vendor/tree-sitter-proto/node_modules/node-addon-api'
|
||||
*
|
||||
* (See https://github.com/abhigyanpatwari/GitNexus/issues/836.)
|
||||
*
|
||||
* We stripped `dependencies` and the `install` script from the vendored
|
||||
* package.json, hoisted `node-addon-api` and `node-gyp-build` into
|
||||
* gitnexus's own optionalDependencies, and moved native compilation here.
|
||||
*
|
||||
* What this does:
|
||||
* Runs `npx node-gyp rebuild` inside `node_modules/tree-sitter-proto/`.
|
||||
* Build output lands in
|
||||
* `node_modules/tree-sitter-proto/build/Release/tree_sitter_proto_binding.node`
|
||||
* — under npm-managed territory, safe on upgrade.
|
||||
*
|
||||
* Mirrors the tree-sitter-dart build helper. Best-effort: if any
|
||||
* precondition fails (optional dep absent, no toolchain, --ignore-scripts),
|
||||
* warn and exit 0 so gitnexus install still succeeds.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Opt-out: skip the native rebuild entirely. Proto parsing becomes
|
||||
// unavailable but `npm install gitnexus` finishes much faster on machines
|
||||
// without a C++ toolchain. Strict `=== '1'` only — '=true', '=yes', '=0'
|
||||
// (read as a string), and any other value all fall through to the rebuild.
|
||||
if (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1') {
|
||||
console.warn(
|
||||
'[tree-sitter-proto] Skipping build (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1). Proto parsing will be unavailable until reinstalled without the env var.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const protoDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-proto');
|
||||
const bindingGyp = path.join(protoDir, 'binding.gyp');
|
||||
const bindingNode = path.join(protoDir, 'build', 'Release', 'tree_sitter_proto_binding.node');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(bindingGyp)) {
|
||||
// tree-sitter-proto is an optionalDependency; absent when install
|
||||
// skipped optional deps or the file: dep was not resolved.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Skip if the native binding already exists (idempotent re-run).
|
||||
if (fs.existsSync(bindingNode)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Pre-flight: the hoisted build deps must be resolvable.
|
||||
try {
|
||||
require.resolve('node-addon-api');
|
||||
require.resolve('node-gyp-build');
|
||||
} catch (resolveErr) {
|
||||
console.warn(
|
||||
'[tree-sitter-proto] Skipping build: hoisted build deps not resolvable (%s).',
|
||||
resolveErr.message,
|
||||
);
|
||||
console.warn(
|
||||
'[tree-sitter-proto] Proto parsing will be unavailable. Install without --no-optional and with scripts enabled to build.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('[tree-sitter-proto] Building native binding...');
|
||||
execSync('npx node-gyp rebuild', {
|
||||
cwd: protoDir,
|
||||
stdio: 'pipe',
|
||||
timeout: 180000,
|
||||
});
|
||||
console.log('[tree-sitter-proto] Native binding built successfully');
|
||||
} catch (err) {
|
||||
console.warn('[tree-sitter-proto] Could not build native binding:', err.message);
|
||||
console.warn(
|
||||
'[tree-sitter-proto] Proto (.proto) parsing will be unavailable. Non-proto gitnexus functionality is unaffected.',
|
||||
);
|
||||
// Exit 0: optionalDependency failures must not fail the gitnexus install.
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Probe tree-sitter-swift prebuild availability at install time.
|
||||
*
|
||||
* The vendored package ships platform prebuilds; node-gyp-build selects the
|
||||
* correct binary at require time. This script calls node-gyp-build once
|
||||
* against the materialized package so a missing-prebuild failure surfaces
|
||||
* as an install-time warning (with the rest of the gitnexus install
|
||||
* succeeding) rather than as a runtime error the first time Swift parsing
|
||||
* is requested. The result is discarded — it does not copy, register, or
|
||||
* mutate anything; the runtime require() path in parser-loader does the
|
||||
* actual load. Running this probe here instead of an npm `install` script
|
||||
* on the vendored package preserves the #836 hygiene (no scripts.install
|
||||
* inside vendor/).
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
if (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1') {
|
||||
console.warn('[tree-sitter-swift] Skipping prebuild probe (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1).');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const swiftDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-swift');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(path.join(swiftDir, 'bindings', 'node', 'index.js'))) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const nodeGypBuild = require('node-gyp-build');
|
||||
nodeGypBuild(swiftDir);
|
||||
} catch (err) {
|
||||
console.warn('[tree-sitter-swift] Prebuild probe failed:', err.message);
|
||||
console.warn(
|
||||
'[tree-sitter-swift] Swift parsing will be unavailable. Non-Swift functionality is unaffected.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* CI helper — emits the `MIGRATED_LANGUAGES` set as a JSON matrix array for
|
||||
* GitHub Actions (`.github/workflows/ci-scope-parity.yml`).
|
||||
*
|
||||
* Consumed by the `discover` job in that workflow. Each entry has:
|
||||
* - `slug`: lowercase language id, matching `test/integration/resolvers/<slug>.test.ts`.
|
||||
* - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_<envvar>` toggle.
|
||||
*
|
||||
* Run with `npx tsx scripts/ci-list-migrated-languages.ts`. The script
|
||||
* writes a single JSON array to stdout (no wrapper object) so the
|
||||
* workflow can pipe it straight into `$GITHUB_OUTPUT`.
|
||||
*/
|
||||
|
||||
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
||||
|
||||
const entries = [...MIGRATED_LANGUAGES].map((slug) => {
|
||||
const s = String(slug);
|
||||
return {
|
||||
slug: s,
|
||||
envvar: s.toUpperCase().replace(/-/g, '_'),
|
||||
};
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify(entries));
|
||||
@@ -33,7 +33,7 @@ const PLATFORM_LOGIC = [
|
||||
'test/unit/resolve-invocation.test.ts',
|
||||
'test/unit/platform-capabilities.test.ts',
|
||||
'test/unit/worker-pool-windows-quarantine.test.ts',
|
||||
'test/unit/lbug-pool-fts-load.test.ts',
|
||||
'test/unit/lbug-pool-win-fts-probe.test.ts',
|
||||
'test/unit/repo-manager.test.ts',
|
||||
'test/unit/repo-manager-finalize-invariant.test.ts',
|
||||
'test/unit/hooks.test.ts',
|
||||
|
||||
@@ -14,7 +14,7 @@ function parseLbugMaxDbSize(raw) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
async function installDuckDbExtension(extensionName, verifyOnly = false) {
|
||||
async function installDuckDbExtension(extensionName) {
|
||||
if (!extensionName || !EXTENSION_NAME_PATTERN.test(extensionName)) {
|
||||
throw new Error(`Invalid DuckDB extension name: ${extensionName ?? '<missing>'}`);
|
||||
}
|
||||
@@ -22,11 +22,9 @@ async function installDuckDbExtension(extensionName, verifyOnly = false) {
|
||||
const require = createRequire(import.meta.url);
|
||||
const lbugModule = require('@ladybugdb/core');
|
||||
const lbug = lbugModule.default ?? lbugModule;
|
||||
// argv[3] is the optional positional size; ignore it when it is actually a
|
||||
// flag token (e.g. `--verify-only`) and fall back to the env default.
|
||||
const sizeArg =
|
||||
process.argv[3] && !process.argv[3].startsWith('--') ? process.argv[3] : undefined;
|
||||
const lbugMaxDbSize = parseLbugMaxDbSize(sizeArg ?? process.env.GITNEXUS_LBUG_MAX_DB_SIZE);
|
||||
const lbugMaxDbSize = parseLbugMaxDbSize(
|
||||
process.argv[3] ?? process.env.GITNEXUS_LBUG_MAX_DB_SIZE,
|
||||
);
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ext-install-'));
|
||||
const dbPath = path.join(tmpDir, 'install.lbug');
|
||||
@@ -36,18 +34,7 @@ async function installDuckDbExtension(extensionName, verifyOnly = false) {
|
||||
try {
|
||||
db = new lbug.Database(dbPath, 0, false, false, lbugMaxDbSize);
|
||||
conn = new lbug.Connection(db);
|
||||
if (verifyOnly) {
|
||||
// Prove a previously-baked extension is resolvable by a FRESH process
|
||||
// under the current HOME (the runtime `LOAD EXTENSION` path) — no INSTALL,
|
||||
// no network. Used as a Docker build-time gate so a HOME/extension-dir
|
||||
// mismatch fails the build instead of silently degrading search at runtime.
|
||||
await conn.query(`LOAD EXTENSION ${extensionName}`);
|
||||
console.log(
|
||||
`[install-ext] LOAD-only verify OK for '${extensionName}' (HOME=${process.env.HOME})`,
|
||||
);
|
||||
} else {
|
||||
await conn.query(`INSTALL ${extensionName}`);
|
||||
}
|
||||
await conn.query(`INSTALL ${extensionName}`);
|
||||
} finally {
|
||||
if (conn) await conn.close().catch(() => {});
|
||||
if (db) await db.close().catch(() => {});
|
||||
@@ -55,10 +42,7 @@ async function installDuckDbExtension(extensionName, verifyOnly = false) {
|
||||
}
|
||||
}
|
||||
|
||||
installDuckDbExtension(
|
||||
process.argv[2] ?? process.env.GITNEXUS_LBUG_EXTENSION_NAME,
|
||||
process.argv.includes('--verify-only'),
|
||||
).catch((err) => {
|
||||
installDuckDbExtension(process.argv[2] ?? process.env.GITNEXUS_LBUG_EXTENSION_NAME).catch((err) => {
|
||||
console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -13,28 +13,14 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
// tree-sitter-c is a REQUIRED grammar that we vendor prebuild-only purely to
|
||||
// close upstream's ARM prebuild gap (#2116) — it needs no toolchain and is not a
|
||||
// language the user opts out of, so it is always materialized, even under
|
||||
// GITNEXUS_SKIP_OPTIONAL_GRAMMARS. The rest are optional (user-skippable, and
|
||||
// Dart/Proto compile from source) and honor the skip flag.
|
||||
const REQUIRED_VENDORED = ['tree-sitter-c'];
|
||||
const OPTIONAL_VENDORED = [
|
||||
'tree-sitter-dart',
|
||||
'tree-sitter-proto',
|
||||
'tree-sitter-swift',
|
||||
'tree-sitter-kotlin',
|
||||
];
|
||||
const VENDORED_GRAMMARS = ['tree-sitter-dart', 'tree-sitter-proto', 'tree-sitter-swift'];
|
||||
|
||||
const skipOptional = process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1';
|
||||
if (skipOptional) {
|
||||
if (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1') {
|
||||
console.warn(
|
||||
'[gitnexus] GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1: skipping optional Dart/Proto/Swift/Kotlin materialize (required C is still materialized).',
|
||||
'[gitnexus] Skipping vendored grammar materialize (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1). Dart/Proto/Swift parsing will be unavailable.',
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const VENDORED_GRAMMARS = skipOptional
|
||||
? REQUIRED_VENDORED
|
||||
: [...REQUIRED_VENDORED, ...OPTIONAL_VENDORED];
|
||||
|
||||
for (const name of VENDORED_GRAMMARS) {
|
||||
const src = path.join(ROOT, 'vendor', name);
|
||||
@@ -63,31 +49,20 @@ for (const name of VENDORED_GRAMMARS) {
|
||||
fs.renameSync(partial, dest);
|
||||
} catch (renameErr) {
|
||||
// Best-effort rollback: restore the previous dest from backup.
|
||||
let restored = false;
|
||||
if (fs.existsSync(backup)) {
|
||||
try {
|
||||
fs.renameSync(backup, dest);
|
||||
restored = true;
|
||||
} catch {
|
||||
// Rollback also failed — dest is now missing. Leave the backup in
|
||||
// place (the catch below will NOT remove it) and surface where it is.
|
||||
// If rollback also fails, the prior backup directory still exists on
|
||||
// disk — the catch block below surfaces both errors via the warning.
|
||||
}
|
||||
}
|
||||
if (!restored && fs.existsSync(backup)) {
|
||||
console.warn(
|
||||
`[gitnexus] CRITICAL: could not materialize vendor/${name} AND could not restore the ` +
|
||||
`previous node_modules/${name}. A recoverable copy remains at ${backup} — ` +
|
||||
`restore it (e.g. \`mv ${backup} ${dest}\`) or reinstall to recover ${name}.`,
|
||||
);
|
||||
}
|
||||
throw renameErr;
|
||||
}
|
||||
fs.rmSync(backup, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
// Fail-soft: a single locked/inaccessible file (common on Windows) must not
|
||||
// abort the whole gitnexus install. Matches build-tree-sitter-*.cjs pattern.
|
||||
// Only remove the scratch `partial`; never the `backup` (it may be the sole
|
||||
// recoverable copy after a failed rollback above).
|
||||
fs.rmSync(partial, { recursive: true, force: true });
|
||||
console.warn(`[gitnexus] Could not materialize vendor/${name}: ${err.message}`);
|
||||
console.warn(
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Consolidated scope-resolution parity runner.
|
||||
*
|
||||
* Replaces the per-language matrix in ci-scope-parity.yml with a single
|
||||
* job that runs all migrated languages sequentially in one process. This
|
||||
* eliminates 8× redundant checkout + npm ci + build cycles (the old
|
||||
* workflow created a separate GitHub Actions job per language).
|
||||
*
|
||||
* For each language in MIGRATED_LANGUAGES:
|
||||
* 1. Run its resolver test with REGISTRY_PRIMARY_<LANG>=0 (legacy DAG)
|
||||
* 2. Run its resolver test with REGISTRY_PRIMARY_<LANG>=1 (registry-primary)
|
||||
*
|
||||
* Both modes must pass. Failures are collected and reported at the end
|
||||
* so all regressions are visible in a single CI run (equivalent to the
|
||||
* old workflow's fail-fast: false behavior).
|
||||
*
|
||||
* Vitest output streams to the console in real time (stdio: 'inherit')
|
||||
* so CI logs show the actual test output directly. No per-invocation
|
||||
* timeout — the CI job-level timeout (30 min) is the outer guard.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/run-parity.ts
|
||||
* npx tsx scripts/run-parity.ts --language python # single language
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
interface ParityFailure {
|
||||
lang: string;
|
||||
mode: 'legacy' | 'registry-primary';
|
||||
}
|
||||
|
||||
function envVarName(slug: string): string {
|
||||
return `REGISTRY_PRIMARY_${slug.toUpperCase().replace(/-/g, '_')}`;
|
||||
}
|
||||
|
||||
function testFilePaths(slug: string): string[] {
|
||||
const resolverDir = path.resolve(ROOT, 'test/integration/resolvers');
|
||||
const files = fs.readdirSync(resolverDir);
|
||||
const direct = `${slug}.test.ts`;
|
||||
const prefixed = `${slug}-`;
|
||||
return files
|
||||
.filter((name) => name === direct || (name.startsWith(prefixed) && name.endsWith('.test.ts')))
|
||||
.sort()
|
||||
.map((name) => `test/integration/resolvers/${name}`);
|
||||
}
|
||||
|
||||
function runVitest(testFile: string, env: Record<string, string>): boolean {
|
||||
try {
|
||||
execFileSync('npx', ['vitest', 'run', testFile], {
|
||||
cwd: ROOT,
|
||||
env: { ...process.env, ...env },
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
const args = process.argv.slice(2);
|
||||
const langFlag = args.indexOf('--language');
|
||||
const singleLang = langFlag >= 0 ? args[langFlag + 1] : undefined;
|
||||
|
||||
if (langFlag >= 0 && singleLang === undefined) {
|
||||
console.error('--language requires a value');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const languages = singleLang ? [singleLang] : [...MIGRATED_LANGUAGES].map(String);
|
||||
|
||||
// Verify test files exist before running
|
||||
const missingFiles: string[] = [];
|
||||
const filesByLanguage = new Map<string, string[]>();
|
||||
for (const lang of languages) {
|
||||
const files = testFilePaths(lang);
|
||||
filesByLanguage.set(lang, files);
|
||||
if (files.length === 0) {
|
||||
missingFiles.push(`test/integration/resolvers/${lang}*.test.ts (${lang})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.error('Missing resolver test files:');
|
||||
for (const f of missingFiles) console.error(` ${f}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Scope-resolution parity: ${languages.length} language(s)`);
|
||||
console.log(`Languages: ${languages.join(', ')}\n`);
|
||||
|
||||
const failures: ParityFailure[] = [];
|
||||
|
||||
for (const lang of languages) {
|
||||
const files = filesByLanguage.get(lang) ?? [];
|
||||
const envVar = envVarName(lang);
|
||||
|
||||
console.log(`\n── ${lang} — legacy DAG (${envVar}=0) ──`);
|
||||
for (const file of files) {
|
||||
if (!runVitest(file, { [envVar]: '0' })) {
|
||||
failures.push({ lang, mode: 'legacy' });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n── ${lang} — registry-primary (${envVar}=1) ──`);
|
||||
for (const file of files) {
|
||||
if (!runVitest(file, { [envVar]: '1' })) {
|
||||
failures.push({ lang, mode: 'registry-primary' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
const total = [...filesByLanguage.values()].reduce((sum, files) => sum + files.length * 2, 0);
|
||||
const passed = total - failures.length;
|
||||
|
||||
console.log('\n═══════════════════════════════════════');
|
||||
console.log('PARITY SUMMARY');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log(`Passed: ${passed}/${total}`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log(`\nFAILURES (${failures.length}):`);
|
||||
for (const f of failures) {
|
||||
console.log(` ✗ ${f.lang} [${f.mode}]`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nAll parity checks passed.');
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* Spike S1 (issue #2080, M0) — THROWAWAY benchmark. Not part of the build
|
||||
* (scripts/ is excluded from tsconfig) or the test suite.
|
||||
*
|
||||
* Question: can LadybugDB serve the headline REACHING_DEF query
|
||||
* [:REACHING_DEF*1..5 {variable}]
|
||||
* fast enough, and what is the right storage shape for the `variable`?
|
||||
*
|
||||
* What it does:
|
||||
* 1. Builds a synthetic ~100K-edge graph of BasicBlock nodes + REACHING_DEF
|
||||
* edges (variable carried in the CodeRelation `reason` column) with a
|
||||
* realistic per-variable fan-out distribution, and loads it through the
|
||||
* real bulk-COPY path (loadGraphToLbug).
|
||||
* 2. Probes whether LadybugDB supports a secondary index on a relationship
|
||||
* property (the crux of the "edge property vs side table" decision).
|
||||
* 3. Times the variable-filtered bounded var-length path query.
|
||||
*
|
||||
* Run: npx tsx scripts/spikes/s1-reaching-def-index-bench.ts [edgeCount]
|
||||
*/
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
||||
import type { KnowledgeGraph } from '../../src/core/graph/types.js';
|
||||
|
||||
const EDGE_COUNT = Number(process.argv[2] ?? 30_000);
|
||||
// Realistic-ish def-use shape: many short chains, variables reused across them.
|
||||
const CHAIN_LEN = 6; // blocks per function-ish chain
|
||||
const DISTINCT_VARS = Math.max(1, Math.floor(EDGE_COUNT / 20)); // ~20 edges/variable fan-out
|
||||
|
||||
const log = (m: string) => process.stdout.write(m + '\n');
|
||||
|
||||
function buildSynthGraph(edgeCount: number): KnowledgeGraph {
|
||||
const g = createKnowledgeGraph();
|
||||
let edges = 0;
|
||||
let chain = 0;
|
||||
while (edges < edgeCount) {
|
||||
const base = `BasicBlock:synth/f${chain}.ts`;
|
||||
for (let i = 0; i <= CHAIN_LEN; i++) {
|
||||
g.addNode({
|
||||
id: `${base}:${i}`,
|
||||
label: 'BasicBlock',
|
||||
properties: {
|
||||
name: '',
|
||||
filePath: `synth/f${chain}.ts`,
|
||||
startLine: i,
|
||||
endLine: i,
|
||||
text: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < CHAIN_LEN && edges < edgeCount; i++) {
|
||||
const variable = `v${edges % DISTINCT_VARS}`;
|
||||
g.addRelationship({
|
||||
id: `${base}:${i}->${i + 1}:${variable}`,
|
||||
sourceId: `${base}:${i}`,
|
||||
targetId: `${base}:${i + 1}`,
|
||||
type: 'REACHING_DEF',
|
||||
confidence: 1.0,
|
||||
reason: variable, // M0 storage: variable rides `reason`
|
||||
});
|
||||
edges++;
|
||||
}
|
||||
chain++;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tmp = path.join(os.tmpdir(), `s1-spike-${Date.now()}`);
|
||||
const storagePath = path.join(tmp, '.gitnexus');
|
||||
const dbPath = path.join(storagePath, 'lbug');
|
||||
await fs.mkdir(dbPath, { recursive: true });
|
||||
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
log(
|
||||
`[S1] building synthetic graph: ~${EDGE_COUNT} REACHING_DEF edges, ` +
|
||||
`${DISTINCT_VARS} distinct variables (~20 edges/var fan-out), chains of ${CHAIN_LEN}`,
|
||||
);
|
||||
const g = buildSynthGraph(EDGE_COUNT);
|
||||
|
||||
let t = performance.now();
|
||||
await adapter.loadGraphToLbug(g, tmp, storagePath);
|
||||
const loadMs = performance.now() - t;
|
||||
const stats = await adapter.getLbugStats();
|
||||
log(`[S1] bulk-COPY load: ${loadMs.toFixed(0)}ms (nodes=${stats.nodes}, edges=${stats.edges})`);
|
||||
|
||||
// (2) Probe: does LadybugDB support a secondary index on a REL property?
|
||||
let relIndexSupported = false;
|
||||
let relIndexErr = '';
|
||||
for (const stmt of [
|
||||
"CALL CREATE_REL_INDEX('CodeRelation', 'cr_reason_idx', 'reason')",
|
||||
'CREATE INDEX cr_reason_idx ON CodeRelation(reason)',
|
||||
]) {
|
||||
try {
|
||||
await adapter.executeQuery(stmt);
|
||||
relIndexSupported = true;
|
||||
break;
|
||||
} catch (e: any) {
|
||||
relIndexErr = String(e?.message ?? e).split('\n')[0];
|
||||
}
|
||||
}
|
||||
log(
|
||||
`[S1] rel-property secondary index supported? ${relIndexSupported} ` +
|
||||
`(last error: ${relIndexErr})`,
|
||||
);
|
||||
|
||||
// (3a) Single-hop variable filter — the common case M3 runs most.
|
||||
const probeVar = 'v0';
|
||||
t = performance.now();
|
||||
const single = await adapter.executeQuery(
|
||||
`MATCH (a:BasicBlock)-[r:CodeRelation {type: 'REACHING_DEF', reason: '${probeVar}'}]->(b:BasicBlock)
|
||||
RETURN count(r) AS c`,
|
||||
);
|
||||
const singleMs = performance.now() - t;
|
||||
log(`[S1] single-hop variable filter → ${single[0]?.c} edges in ${singleMs.toFixed(0)}ms`);
|
||||
|
||||
// (3b) SOURCE-ANCHORED bounded var-length path — the realistic taint query
|
||||
// (anchor the source block, then walk REACHING_DEF up to 5 hops). The
|
||||
// UNANCHORED global form ([:REACHING_DEF*1..5] from every block) is
|
||||
// impractical at scale (path explosion) — that is itself an S1 finding:
|
||||
// taint queries MUST be scoped to a source block, not run graph-wide.
|
||||
const srcId = 'BasicBlock:synth/f0.ts:0';
|
||||
t = performance.now();
|
||||
const anchored = await adapter.executeQuery(
|
||||
`MATCH p = (a:BasicBlock)-[:CodeRelation*1..5 {type: 'REACHING_DEF'}]->(b:BasicBlock)
|
||||
WHERE a.id = '${srcId}' AND all(rel IN relationships(p) WHERE rel.reason = '${probeVar}')
|
||||
RETURN count(p) AS paths`,
|
||||
);
|
||||
const pathMs = performance.now() - t;
|
||||
log(
|
||||
`[S1] source-anchored [:REACHING_DEF*1..5 {reason='${probeVar}'}] from one block → ` +
|
||||
`${anchored[0]?.paths} paths in ${pathMs.toFixed(0)}ms`,
|
||||
);
|
||||
|
||||
await adapter.closeLbug();
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
|
||||
log('\n[S1] VERDICT INPUTS:');
|
||||
log(
|
||||
` load_ms=${loadMs.toFixed(0)} single_hop_ms=${singleMs.toFixed(0)} anchored_path_ms=${pathMs.toFixed(0)} rel_index=${relIndexSupported}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('[S1] FAILED:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Spike S2 (issue #2080, M0) — THROWAWAY post-dominator feasibility prototype.
|
||||
* Not part of the build (scripts/ excluded from tsconfig) or the test suite.
|
||||
*
|
||||
* Question (per maintainer review): does the post-dominator algorithm Epic B
|
||||
* (#2085, CDG) depends on hold up on real TS/JS control-flow shapes — the
|
||||
* classic CFG hazards — before Epic B commits to it?
|
||||
*
|
||||
* Scope boundary: post-dominators operate on a CFG, not on the AST directly.
|
||||
* This prototype validates the ALGORITHM (iterative dataflow on the reverse
|
||||
* CFG, EXIT-rooted, → immediate-post-dominator tree) against CFGs that model
|
||||
* each hazard's real TS control flow (the TS source each CFG represents is
|
||||
* shown inline). Building the CFG from a tree-sitter AST is M1's job (#2081);
|
||||
* this spike deliberately does not reimplement it.
|
||||
*
|
||||
* Run: npx tsx scripts/spikes/s2-postdom-prototype.ts
|
||||
*/
|
||||
|
||||
type CFG = {
|
||||
name: string;
|
||||
tsSource: string;
|
||||
entry: string;
|
||||
exit: string;
|
||||
// adjacency: block -> successors
|
||||
succ: Record<string, string[]>;
|
||||
hazard: string;
|
||||
};
|
||||
|
||||
// Iterative post-dominator dataflow on the reverse CFG.
|
||||
// PostDom(EXIT) = {EXIT}; PostDom(n) = {n} ∪ (⋂ PostDom(s) for s ∈ succ(n)).
|
||||
// Monotone over a finite lattice (powerset of blocks) ⇒ guaranteed to converge.
|
||||
function postDominators(cfg: CFG): { pdom: Record<string, Set<string>>; iterations: number } {
|
||||
const blocks = Object.keys(cfg.succ);
|
||||
const all = new Set(blocks);
|
||||
const pdom: Record<string, Set<string>> = {};
|
||||
for (const b of blocks) pdom[b] = b === cfg.exit ? new Set([cfg.exit]) : new Set(all);
|
||||
|
||||
let changed = true;
|
||||
let iterations = 0;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
iterations++;
|
||||
for (const b of blocks) {
|
||||
if (b === cfg.exit) continue;
|
||||
const succs = cfg.succ[b] ?? [];
|
||||
let inter: Set<string> | null = null;
|
||||
for (const s of succs) {
|
||||
if (inter === null) inter = new Set(pdom[s]);
|
||||
else inter = new Set([...inter].filter((x) => pdom[s].has(x)));
|
||||
}
|
||||
const next = new Set<string>(inter ?? []);
|
||||
next.add(b);
|
||||
if (next.size !== pdom[b].size || [...next].some((x) => !pdom[b].has(x))) {
|
||||
pdom[b] = next;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (iterations > blocks.length + 5)
|
||||
throw new Error('post-dom did not converge (suspected bug)');
|
||||
}
|
||||
return { pdom, iterations };
|
||||
}
|
||||
|
||||
// Immediate post-dominator: the closest strict post-dominator.
|
||||
function ipdom(cfg: CFG, pdom: Record<string, Set<string>>): Record<string, string | null> {
|
||||
const res: Record<string, string | null> = {};
|
||||
for (const b of Object.keys(cfg.succ)) {
|
||||
if (b === cfg.exit) {
|
||||
res[b] = null;
|
||||
continue;
|
||||
}
|
||||
const strict = [...pdom[b]].filter((x) => x !== b);
|
||||
// ipdom = the strict post-dom that does not post-dominate any other strict post-dom.
|
||||
res[b] =
|
||||
strict.find((cand) => strict.every((other) => other === cand || !pdom[other].has(cand))) ??
|
||||
null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const CFGS: CFG[] = [
|
||||
{
|
||||
name: 'early-return',
|
||||
hazard: 'early return / multiple paths to EXIT',
|
||||
tsSource: `function f(x){ if (x) { return 1; } g(); return 2; }`,
|
||||
entry: 'ENTRY',
|
||||
exit: 'EXIT',
|
||||
succ: { ENTRY: ['ret1', 'g'], ret1: ['EXIT'], g: ['ret2'], ret2: ['EXIT'], EXIT: [] },
|
||||
},
|
||||
{
|
||||
name: 'try-throw-finally',
|
||||
hazard: 'try/throw/finally with multiple exits through finally',
|
||||
tsSource: `function f(){ try { risky(); } catch(e){ handle(e); } finally { cleanup(); } done(); }`,
|
||||
entry: 'ENTRY',
|
||||
exit: 'EXIT',
|
||||
// try → (normal | throw→catch) → finally → done → EXIT; finally also reached on rethrow
|
||||
succ: {
|
||||
ENTRY: ['try'],
|
||||
try: ['finally', 'catch'],
|
||||
catch: ['finally'],
|
||||
finally: ['done', 'EXIT'],
|
||||
done: ['EXIT'],
|
||||
EXIT: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'labeled-break',
|
||||
hazard: 'labeled break/continue across nested loops',
|
||||
tsSource: `outer: for(;;){ for(;;){ if (a) break outer; if (b) continue outer; work(); } }`,
|
||||
entry: 'ENTRY',
|
||||
exit: 'EXIT',
|
||||
succ: {
|
||||
ENTRY: ['outerHead'],
|
||||
outerHead: ['innerHead', 'EXIT'],
|
||||
innerHead: ['breakOuter', 'afterIf1'],
|
||||
breakOuter: ['EXIT'],
|
||||
afterIf1: ['contOuter', 'work'],
|
||||
contOuter: ['outerHead'],
|
||||
work: ['innerHead'],
|
||||
EXIT: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'if-else-diamond',
|
||||
hazard: 'baseline reducible diamond (sanity)',
|
||||
tsSource: `function f(x){ if (x) { a(); } else { b(); } c(); }`,
|
||||
entry: 'ENTRY',
|
||||
exit: 'EXIT',
|
||||
succ: { ENTRY: ['a', 'b'], a: ['c'], b: ['c'], c: ['EXIT'], EXIT: [] },
|
||||
},
|
||||
];
|
||||
|
||||
function main() {
|
||||
let allOk = true;
|
||||
for (const cfg of CFGS) {
|
||||
try {
|
||||
const { pdom, iterations } = postDominators(cfg);
|
||||
const idom = ipdom(cfg, pdom);
|
||||
// Sanity invariants: EXIT post-dominates every block; ipdom tree reaches EXIT.
|
||||
const exitPostDomsAll = Object.keys(cfg.succ).every((b) => pdom[b].has(cfg.exit));
|
||||
console.log(`\n[S2] ${cfg.name} — ${cfg.hazard}`);
|
||||
console.log(` TS: ${cfg.tsSource}`);
|
||||
console.log(
|
||||
` converged in ${iterations} iters; EXIT post-dominates all blocks: ${exitPostDomsAll}`,
|
||||
);
|
||||
console.log(
|
||||
` ipdom tree: ${Object.entries(idom)
|
||||
.map(([b, p]) => `${b}->${p ?? '∅'}`)
|
||||
.join(' ')}`,
|
||||
);
|
||||
if (!exitPostDomsAll) allOk = false;
|
||||
} catch (e) {
|
||||
allOk = false;
|
||||
console.log(`\n[S2] ${cfg.name} FAILED: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`\n[S2] VERDICT INPUT: all hazard CFGs converged + EXIT post-dominates all = ${allOk}`,
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,291 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>GitNexus — Shadow Parity Dashboard</title>
|
||||
<!--
|
||||
Static dashboard for the RFC #909 shadow-mode parity report.
|
||||
|
||||
Reads `latest.json` from this directory and renders a per-language
|
||||
parity table. Zero build step, zero runtime dependencies — a
|
||||
single file that any browser or file:// context can open.
|
||||
|
||||
Usage:
|
||||
# from repo root, after a shadow-mode run
|
||||
cp .gitnexus/shadow-parity/latest.json gitnexus/shadow-parity-dashboard/
|
||||
open gitnexus/shadow-parity-dashboard/index.html
|
||||
|
||||
CI artifact wiring (follow-up): the CI job publishes a snapshot
|
||||
of this directory + latest.json as a downloadable bundle per run.
|
||||
-->
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--fg: #1f2937;
|
||||
--fg-muted: #6b7280;
|
||||
--bg: #ffffff;
|
||||
--bg-muted: #f9fafb;
|
||||
--border: #e5e7eb;
|
||||
--good: #16a34a;
|
||||
--warn: #d97706;
|
||||
--bad: #dc2626;
|
||||
--primary-tag-legacy: #7c3aed;
|
||||
--primary-tag-registry: #0ea5e9;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fg: #e5e7eb;
|
||||
--fg-muted: #9ca3af;
|
||||
--bg: #111827;
|
||||
--bg-muted: #1f2937;
|
||||
--border: #374151;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font:
|
||||
14px/1.45 system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
}
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.meta {
|
||||
color: var(--fg-muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
.card .k {
|
||||
color: var(--fg-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.card .v {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
thead th {
|
||||
font-weight: 600;
|
||||
color: var(--fg-muted);
|
||||
font-size: 12px;
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: var(--bg-muted);
|
||||
}
|
||||
.parity {
|
||||
font-weight: 600;
|
||||
}
|
||||
.parity.good {
|
||||
color: var(--good);
|
||||
}
|
||||
.parity.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
.parity.bad {
|
||||
color: var(--bad);
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
margin-left: 6px;
|
||||
color: white;
|
||||
}
|
||||
.tag.legacy {
|
||||
background: var(--primary-tag-legacy);
|
||||
}
|
||||
.tag.registry {
|
||||
background: var(--primary-tag-registry);
|
||||
}
|
||||
.empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
background: var(--bg-muted);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Shadow Parity — RFC #909</h1>
|
||||
<div class="meta" id="meta">loading <code>latest.json</code>…</div>
|
||||
<div class="cards" id="cards"></div>
|
||||
<table id="per-language">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Language</th>
|
||||
<th>Total</th>
|
||||
<th>Agree</th>
|
||||
<th>Only legacy</th>
|
||||
<th>Only new</th>
|
||||
<th>Disagree</th>
|
||||
<th>Both empty</th>
|
||||
<th>Parity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div id="empty" class="empty" style="display: none">
|
||||
No records yet. Enable <code>GITNEXUS_SHADOW_MODE=1</code> and run ingestion to populate.
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
/* global fetch, document */
|
||||
(async function () {
|
||||
const tbody = document.querySelector('#per-language tbody');
|
||||
const cards = document.getElementById('cards');
|
||||
const meta = document.getElementById('meta');
|
||||
const empty = document.getElementById('empty');
|
||||
const table = document.getElementById('per-language');
|
||||
|
||||
let payload;
|
||||
try {
|
||||
const r = await fetch('./latest.json', { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
payload = await r.json();
|
||||
} catch (err) {
|
||||
meta.textContent = 'Failed to load latest.json: ' + err.message;
|
||||
table.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const primary = payload.primaryByLanguage || {};
|
||||
const report = payload.report || {};
|
||||
const perLang = report.perLanguage || [];
|
||||
const overall = report.overall || {};
|
||||
|
||||
meta.textContent =
|
||||
'Run ' +
|
||||
payload.runId +
|
||||
' — generated ' +
|
||||
payload.generatedAt +
|
||||
' (schema v' +
|
||||
payload.schemaVersion +
|
||||
')';
|
||||
|
||||
// Overall summary cards.
|
||||
cards.innerHTML = '';
|
||||
const overallParity = overall.parity !== undefined ? overall.parity : 0;
|
||||
cards.appendChild(makeCard('Total calls', overall.totalCalls ?? 0));
|
||||
cards.appendChild(makeCard('Both agree', overall.bothAgree ?? 0));
|
||||
cards.appendChild(makeCard('Disagree', overall.bothDisagree ?? 0));
|
||||
cards.appendChild(makeCard('Overall parity', formatPct(overallParity)));
|
||||
|
||||
if (!perLang.length) {
|
||||
table.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of perLang) {
|
||||
const tr = document.createElement('tr');
|
||||
const primaryTag = primary[row.language];
|
||||
const tag = primaryTag
|
||||
? '<span class="tag ' + primaryTag + '">primary: ' + primaryTag + '</span>'
|
||||
: '';
|
||||
const parityClass = parityClassFor(row.parity);
|
||||
tr.innerHTML =
|
||||
'<td>' +
|
||||
escape(row.language) +
|
||||
tag +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.totalCalls +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothAgree +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.onlyLegacy +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.onlyNew +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothDisagree +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
row.bothEmpty +
|
||||
'</td>' +
|
||||
'<td class="parity ' +
|
||||
parityClass +
|
||||
'">' +
|
||||
formatPct(row.parity) +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
function makeCard(k, v) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'card';
|
||||
div.innerHTML =
|
||||
'<div class="k">' + escape(k) + '</div><div class="v">' + escape(String(v)) + '</div>';
|
||||
return div;
|
||||
}
|
||||
function formatPct(x) {
|
||||
if (typeof x !== 'number' || !isFinite(x)) return '—';
|
||||
return (x * 100).toFixed(1) + '%';
|
||||
}
|
||||
function parityClassFor(x) {
|
||||
if (typeof x !== 'number') return '';
|
||||
if (x >= 0.95) return 'good';
|
||||
if (x >= 0.8) return 'warn';
|
||||
return 'bad';
|
||||
}
|
||||
function escape(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,10 +16,10 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. context({name: "<suspect>"}) → See callers/callees/processes
|
||||
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
|
||||
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
|
||||
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
|
||||
4. cypher({query: "MATCH path..."}) → Custom traces if needed
|
||||
4. gitnexus_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)
|
||||
- [ ] query for error text or related code
|
||||
- [ ] gitnexus_query for error text or related code
|
||||
- [ ] Identify the suspect function from returned processes
|
||||
- [ ] context to see callers and callees
|
||||
- [ ] gitnexus_context to see callers and callees
|
||||
- [ ] Trace execution flow via process resource if applicable
|
||||
- [ ] cypher for custom call chain traces if needed
|
||||
- [ ] gitnexus_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 | `query` for error text → `context` on throw sites |
|
||||
| Error message | `gitnexus_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
|
||||
|
||||
**query** — find code related to error:
|
||||
**gitnexus_query** — find code related to error:
|
||||
|
||||
```
|
||||
query({query: "payment validation error"})
|
||||
gitnexus_query({query: "payment validation error"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError, PaymentException
|
||||
```
|
||||
|
||||
**context** — full context for a suspect:
|
||||
**gitnexus_context** — full context for a suspect:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
→ Processes: CheckoutFlow (step 3/7)
|
||||
```
|
||||
|
||||
**cypher** — custom call chain traces:
|
||||
**gitnexus_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. query({query: "payment error handling"})
|
||||
1. gitnexus_query({query: "payment error handling"})
|
||||
→ Processes: CheckoutFlow, ErrorHandling
|
||||
→ Symbols: validatePayment, handlePaymentError
|
||||
|
||||
2. context({name: "validatePayment"})
|
||||
2. gitnexus_context({name: "validatePayment"})
|
||||
→ Outgoing calls: verifyCard, fetchRates (external API!)
|
||||
|
||||
3. READ gitnexus://repo/my-app/process/CheckoutFlow
|
||||
|
||||
@@ -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. query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
|
||||
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
|
||||
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
|
||||
```
|
||||
|
||||
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
|
||||
|
||||
```
|
||||
- [ ] READ gitnexus://repo/{name}/context
|
||||
- [ ] query for the concept you want to understand
|
||||
- [ ] gitnexus_query for the concept you want to understand
|
||||
- [ ] Review returned processes (execution flows)
|
||||
- [ ] context on key symbols for callers/callees
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**query** — find execution flows related to a concept:
|
||||
**gitnexus_query** — find execution flows related to a concept:
|
||||
|
||||
```
|
||||
query({query: "payment processing"})
|
||||
gitnexus_query({query: "payment processing"})
|
||||
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Symbols grouped by flow with file locations
|
||||
```
|
||||
|
||||
**context** — 360-degree view of a symbol:
|
||||
**gitnexus_context** — 360-degree view of a symbol:
|
||||
|
||||
```
|
||||
context({name: "validateUser"})
|
||||
gitnexus_context({name: "validateUser"})
|
||||
→ Incoming calls: loginHandler, apiMiddleware
|
||||
→ Outgoing calls: checkToken, getUserById
|
||||
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
|
||||
@@ -68,10 +68,10 @@ context({name: "validateUser"})
|
||||
|
||||
```
|
||||
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
|
||||
2. query({query: "payment processing"})
|
||||
2. gitnexus_query({query: "payment processing"})
|
||||
→ CheckoutFlow: processPayment → validateCard → chargeStripe
|
||||
→ RefundFlow: initiateRefund → calculateRefund → processRefund
|
||||
3. context({name: "processPayment"})
|
||||
3. gitnexus_context({name: "processPayment"})
|
||||
→ Incoming: checkoutHandler, webhookHandler
|
||||
→ Outgoing: validateCard, chargeStripe, saveTransaction
|
||||
4. Read src/payments/processor.ts for implementation details
|
||||
|
||||
@@ -38,38 +38,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
||||
| `detect_changes` | Git-diff impact — what do your current changes affect |
|
||||
| `rename` | Multi-file coordinated rename with confidence-tagged edits |
|
||||
| `cypher` | Raw graph queries (read `gitnexus://repo/{name}/schema` first) |
|
||||
| `list_repos` | Discover indexed repos (paginated — `limit`/`offset`) |
|
||||
|
||||
### Paginating `list_repos`
|
||||
|
||||
`list_repos` is paginated so a large registry is not truncated by MCP/LLM token limits. It takes optional `limit` (default **50**, max **200**) and `offset`, and returns:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"repositories": [
|
||||
{ "name": "...", "path": "...", "indexedAt": "...", "lastCommit": "...", "stats": { } }
|
||||
],
|
||||
"pagination": {
|
||||
"total": 437,
|
||||
"limit": 50,
|
||||
"offset": 0,
|
||||
"returned": 50,
|
||||
"hasMore": true,
|
||||
"nextOffset": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To enumerate **every** repository, keep calling with `offset` set to `pagination.nextOffset` until `hasMore` is `false`:
|
||||
|
||||
```text
|
||||
list_repos {} → repos 1–50, nextOffset 50, hasMore true
|
||||
list_repos { offset: 50 } → repos 51–100, nextOffset 100, hasMore true
|
||||
…
|
||||
list_repos { offset: 400 } → repos 401–437, hasMore false (done)
|
||||
```
|
||||
|
||||
Notes: `offset` ≥ `total` returns an empty page (with `total` still reported). Out-of-range or malformed `limit`/`offset` (non-integer, `limit` outside `[1, 200]`, `offset < 0`) are rejected with a clear error — `limit` above the max is rejected, not silently capped. The order is deterministic (lower-cased name, then path), so paging never skips or duplicates an entry while the registry is unchanged.
|
||||
| `list_repos` | Discover indexed repos |
|
||||
|
||||
## Resources Reference
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ description: "Use when the user wants to know what will break if they change som
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
|
||||
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
3. detect_changes() → Map current git changes to affected flows
|
||||
3. gitnexus_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
|
||||
|
||||
```
|
||||
- [ ] impact({target, direction: "upstream"}) to find dependents
|
||||
- [ ] gitnexus_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
|
||||
- [ ] detect_changes() for pre-commit check
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**impact** — the primary tool for symbol blast radius:
|
||||
**gitnexus_impact** — the primary tool for symbol blast radius:
|
||||
|
||||
```
|
||||
impact({
|
||||
gitnexus_impact({
|
||||
target: "validateUser",
|
||||
direction: "upstream",
|
||||
minConfidence: 0.8,
|
||||
@@ -73,10 +73,10 @@ impact({
|
||||
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**detect_changes** — git-diff based impact analysis:
|
||||
**gitnexus_detect_changes** — git-diff based impact analysis:
|
||||
|
||||
```
|
||||
detect_changes({scope: "staged"})
|
||||
gitnexus_detect_changes({scope: "staged"})
|
||||
|
||||
→ Changed: 5 symbols in 3 files
|
||||
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
|
||||
@@ -86,7 +86,7 @@ detect_changes({scope: "staged"})
|
||||
## Example: "What breaks if I change validateUser?"
|
||||
|
||||
```
|
||||
1. impact({target: "validateUser", direction: "upstream"})
|
||||
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
|
||||
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
1. gh pr diff <number> → Get the raw diff
|
||||
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
|
||||
3. For each changed symbol:
|
||||
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. context({name: "<key symbol>"}) → Understand callers/callees
|
||||
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
|
||||
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
|
||||
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
|
||||
6. Summarize findings with risk assessment
|
||||
```
|
||||
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
|
||||
|
||||
```
|
||||
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
|
||||
- [ ] detect_changes to map changes to affected execution flows
|
||||
- [ ] impact on each non-trivial changed symbol
|
||||
- [ ] gitnexus_detect_changes to map changes to affected execution flows
|
||||
- [ ] gitnexus_impact on each non-trivial changed symbol
|
||||
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
|
||||
- [ ] context on key changed symbols to understand full picture
|
||||
- [ ] gitnexus_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
|
||||
|
||||
**detect_changes** — map PR diff to affected execution flows:
|
||||
**gitnexus_detect_changes** — map PR diff to affected execution flows:
|
||||
|
||||
```
|
||||
detect_changes({scope: "compare", base_ref: "main"})
|
||||
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
|
||||
→ Changed: 8 symbols in 4 files
|
||||
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**impact** — blast radius per changed symbol:
|
||||
**gitnexus_impact** — blast radius per changed symbol:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream"})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
|
||||
→ d=1 (WILL BREAK):
|
||||
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
|
||||
@@ -86,20 +86,20 @@ impact({target: "validatePayment", direction: "upstream"})
|
||||
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
|
||||
```
|
||||
|
||||
**impact with tests** — check test coverage:
|
||||
**gitnexus_impact with tests** — check test coverage:
|
||||
|
||||
```
|
||||
impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
|
||||
|
||||
→ Tests that cover this symbol:
|
||||
- validatePayment.test.ts [direct]
|
||||
- checkout.integration.test.ts [via processCheckout]
|
||||
```
|
||||
|
||||
**context** — understand a changed symbol's role:
|
||||
**gitnexus_context** — understand a changed symbol's role:
|
||||
|
||||
```
|
||||
context({name: "validatePayment"})
|
||||
gitnexus_context({name: "validatePayment"})
|
||||
|
||||
→ Incoming calls: processCheckout, webhookHandler
|
||||
→ Outgoing calls: verifyCard, fetchRates
|
||||
@@ -112,20 +112,20 @@ context({name: "validatePayment"})
|
||||
1. gh pr diff 42 > /tmp/pr42.diff
|
||||
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
|
||||
|
||||
2. detect_changes({scope: "compare", base_ref: "main"})
|
||||
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
|
||||
→ Changed symbols: validatePayment, PaymentInput, formatAmount
|
||||
→ Affected processes: CheckoutFlow, RefundFlow
|
||||
→ Risk: MEDIUM
|
||||
|
||||
3. impact({target: "validatePayment", direction: "upstream"})
|
||||
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
|
||||
→ d=1: processCheckout, webhookHandler (WILL BREAK)
|
||||
→ webhookHandler is NOT in the PR diff — potential breakage!
|
||||
|
||||
4. impact({target: "PaymentInput", direction: "upstream"})
|
||||
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
|
||||
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
|
||||
→ createPayment uses the old PaymentInput shape — breaking change!
|
||||
|
||||
5. context({name: "formatAmount"})
|
||||
5. gitnexus_context({name: "formatAmount"})
|
||||
→ Called by 12 functions — but change is backwards-compatible (added optional param)
|
||||
|
||||
6. Review summary:
|
||||
|
||||
@@ -16,9 +16,9 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
||||
## Workflow
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
```
|
||||
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
|
||||
- [ ] gitnexus_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: rename({..., dry_run: false}) — apply edits
|
||||
- [ ] detect_changes() — verify only expected files changed
|
||||
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
|
||||
- [ ] gitnexus_detect_changes() — verify only expected files changed
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Extract Module
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
|
||||
- [ ] Define new module interface
|
||||
- [ ] Extract code, update imports
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
### Split Function/Service
|
||||
|
||||
```
|
||||
- [ ] context({name: target}) — understand all callees
|
||||
- [ ] gitnexus_context({name: target}) — understand all callees
|
||||
- [ ] Group callees by responsibility
|
||||
- [ ] impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
|
||||
- [ ] Create new functions/services
|
||||
- [ ] Update callers
|
||||
- [ ] detect_changes() — verify affected scope
|
||||
- [ ] gitnexus_detect_changes() — verify affected scope
|
||||
- [ ] Run tests for affected processes
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
**rename** — automated multi-file rename:
|
||||
**gitnexus_rename** — automated multi-file rename:
|
||||
|
||||
```
|
||||
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
gitnexus_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}]}]
|
||||
```
|
||||
|
||||
**impact** — map all dependents first:
|
||||
**gitnexus_impact** — map all dependents first:
|
||||
|
||||
```
|
||||
impact({target: "validateUser", direction: "upstream"})
|
||||
gitnexus_impact({target: "validateUser", direction: "upstream"})
|
||||
→ d=1: loginHandler, apiMiddleware, testUtils
|
||||
→ Affected Processes: LoginFlow, TokenRefresh
|
||||
```
|
||||
|
||||
**detect_changes** — verify your changes after refactoring:
|
||||
**gitnexus_detect_changes** — verify your changes after refactoring:
|
||||
|
||||
```
|
||||
detect_changes({scope: "all"})
|
||||
gitnexus_detect_changes({scope: "all"})
|
||||
→ Changed: 8 files, 12 symbols
|
||||
→ Affected processes: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM
|
||||
```
|
||||
|
||||
**cypher** — custom reference queries:
|
||||
**gitnexus_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 rename for automated updates |
|
||||
| Many callers (>5) | Use gitnexus_rename for automated updates |
|
||||
| Cross-area refs | Use detect_changes after to verify scope |
|
||||
| String/dynamic refs | query to find them |
|
||||
| String/dynamic refs | gitnexus_query to find them |
|
||||
| External/public API | Version and deprecate properly |
|
||||
|
||||
## Example: Rename `validateUser` to `authenticateUser`
|
||||
|
||||
```
|
||||
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
|
||||
1. gitnexus_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. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
|
||||
→ Applied 12 edits across 8 files
|
||||
|
||||
4. detect_changes({scope: "all"})
|
||||
4. gitnexus_detect_changes({scope: "all"})
|
||||
→ Affected: LoginFlow, TokenRefresh
|
||||
→ Risk: MEDIUM — run tests for these flows
|
||||
```
|
||||
|
||||
@@ -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 \`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 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 warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- 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"})\`.
|
||||
- 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"})\`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running \`impact\` on it.
|
||||
- NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
+21
-75
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import v8 from 'v8';
|
||||
import cliProgress from 'cli-progress';
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
} from './analyze-config.js';
|
||||
import { runFullAnalysis } from '../core/run-analyze.js';
|
||||
import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
|
||||
import { warnMissingOptionalGrammars, getOptionalGrammarExtensions } from './optional-grammars.js';
|
||||
import { warnMissingOptionalGrammars } from './optional-grammars.js';
|
||||
import { glob } from 'glob';
|
||||
import fs from 'fs/promises';
|
||||
import { cliError } from './cli-message.js';
|
||||
@@ -60,22 +59,6 @@ const writeFatalToStderr = (label: string, err: unknown): void => {
|
||||
const message = isErr ? err.message : String(err);
|
||||
realStderrWrite(`\n ${label}: ${message}\n`);
|
||||
if (isErr && err.stack) realStderrWrite(`${err.stack}\n`);
|
||||
// Walk and print the `cause` chain. The phase runner wraps the underlying
|
||||
// failure as `new Error("Phase 'X' failed: …", { cause })`, so the original
|
||||
// error (e.g. a WorkerPoolDispatchError carrying the worker-side stack from
|
||||
// #2068) is only reachable via `.cause`. Without this the user sees the
|
||||
// wrapper's main-thread stack and never the real frame. `cause.stack` already
|
||||
// begins with the cause's message, so we print the stack alone (not message +
|
||||
// stack) to avoid repeating it. Depth-bounded so a cyclic `cause` can't loop
|
||||
// (the phase runner wraps one level; the bound leaves headroom for future
|
||||
// nesting); uses realStderrWrite so the redirected console.error's ANSI
|
||||
// clear-line wrapping can't erase it (#1169).
|
||||
const MAX_CAUSE_DEPTH = 5;
|
||||
let cause: unknown = isErr ? (err as { cause?: unknown }).cause : undefined;
|
||||
for (let depth = 0; depth < MAX_CAUSE_DEPTH && cause instanceof Error; depth++) {
|
||||
realStderrWrite(`\n Caused by: ${cause.stack ?? cause.message}\n`);
|
||||
cause = (cause as { cause?: unknown }).cause;
|
||||
}
|
||||
};
|
||||
|
||||
let fatalHandlersInstalled = false;
|
||||
@@ -101,45 +84,13 @@ const installFatalHandlers = (): void => {
|
||||
});
|
||||
};
|
||||
|
||||
/** Historical floor for the re-exec heap cap — the auto-sizer never goes below
|
||||
* this, so small boxes / CI never regress. */
|
||||
const DEFAULT_HEAP_MB = 16384;
|
||||
|
||||
/**
|
||||
* RAM-aware re-exec heap cap (MB): `0.75 × effective RAM`, clamped to
|
||||
* `>= DEFAULT_HEAP_MB`. Kept BELOW physical RAM on purpose — a cap `>=` RAM makes
|
||||
* V8 collect lazily and inflate the heap into swap-thrash (observed analyzing the
|
||||
* Linux kernel at a 30GB cap on a 31GB box). `constrainedBytes` is the cgroup
|
||||
* limit or `null`; it is honored only as a real, smaller-than-physical cap, because
|
||||
* `process.constrainedMemory()` returns a huge sentinel when UNCONSTRAINED.
|
||||
*/
|
||||
export function computeHeapCapMb(totalBytes: number, constrainedBytes: number | null): number {
|
||||
const effectiveBytes =
|
||||
constrainedBytes !== null && constrainedBytes > 0 && constrainedBytes < totalBytes
|
||||
? constrainedBytes
|
||||
: totalBytes;
|
||||
const effectiveMb = Math.floor(effectiveBytes / (1024 * 1024));
|
||||
return Math.max(DEFAULT_HEAP_MB, Math.floor(0.75 * effectiveMb));
|
||||
}
|
||||
|
||||
function readConstrainedBytes(): number | null {
|
||||
if (typeof process.constrainedMemory !== 'function') return null;
|
||||
const c = process.constrainedMemory();
|
||||
return typeof c === 'number' && c > 0 ? c : null;
|
||||
}
|
||||
|
||||
const HEAP_MB = computeHeapCapMb(os.totalmem(), readConstrainedBytes());
|
||||
const HEAP_MB = 16384;
|
||||
const TEST_RESPAWN_HEAP_MB = Number(process.env.GITNEXUS_TEST_RESPAWN_HEAP_MB);
|
||||
const RESPAWN_HEAP_MB =
|
||||
Number.isFinite(TEST_RESPAWN_HEAP_MB) && TEST_RESPAWN_HEAP_MB > 0
|
||||
? Math.floor(TEST_RESPAWN_HEAP_MB)
|
||||
: HEAP_MB;
|
||||
const HEAP_FLAG = `--max-old-space-size=${RESPAWN_HEAP_MB}`;
|
||||
/** Larger semi-space (young-gen) cuts minor-GC frequency + promotion churn during
|
||||
* the multi-million-node graph build/emit. Allowed in NODE_OPTIONS (unlike
|
||||
* --stack-size), so it propagates to the re-exec env cleanly. */
|
||||
const SEMI_SPACE_MB = 128;
|
||||
const SEMI_FLAG = `--max-semi-space-size=${SEMI_SPACE_MB}`;
|
||||
/** Increase default stack size (KB) to prevent stack overflow on deep class hierarchies. */
|
||||
const STACK_KB = 4096;
|
||||
const STACK_FLAG = `--stack-size=${STACK_KB}`;
|
||||
@@ -489,8 +440,7 @@ const forceHeapOOMForTestIfEnabled = (): void => {
|
||||
// `gitnexus/src/core/lbug/lbug-config.ts` in sync with this value.
|
||||
const RECOMMENDED_WAL_CHECKPOINT_THRESHOLD = 64 * 1024 * 1024;
|
||||
|
||||
/** Re-exec the process with the RAM-aware auto heap cap + larger semi-space/stack
|
||||
* if we're currently below that. A user-supplied NODE_OPTIONS heap wins (no re-exec). */
|
||||
/** Re-exec the process with a 16GB heap and larger stack if we're currently below that. */
|
||||
async function ensureHeap(): Promise<boolean> {
|
||||
const nodeOpts = process.env.NODE_OPTIONS || '';
|
||||
if (nodeOpts.includes('--max-old-space-size')) return false;
|
||||
@@ -498,26 +448,25 @@ async function ensureHeap(): Promise<boolean> {
|
||||
const v8Heap = v8.getHeapStatistics().heap_size_limit;
|
||||
if (v8Heap >= HEAP_MB * 1024 * 1024 * 0.9) return false;
|
||||
|
||||
// --stack-size is a V8 flag not allowed in NODE_OPTIONS on Node 24+, so pass it
|
||||
// only as a direct CLI argument. --max-semi-space-size IS allowed in NODE_OPTIONS.
|
||||
const cliFlags = [HEAP_FLAG, SEMI_FLAG];
|
||||
// --stack-size is a V8 flag not allowed in NODE_OPTIONS on Node 24+,
|
||||
// so pass it only as a direct CLI argument, not via the environment.
|
||||
const cliFlags = [HEAP_FLAG];
|
||||
if (!nodeOpts.includes('--stack-size')) cliFlags.push(STACK_FLAG);
|
||||
|
||||
const childArgs = [...cliFlags, ...process.argv.slice(1)];
|
||||
const childEnv = {
|
||||
...process.env,
|
||||
NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG} ${SEMI_FLAG}`.trim(),
|
||||
NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG}`.trim(),
|
||||
};
|
||||
if (shouldBridgeRespawnProgressTty()) childEnv[RESPAWN_PROGRESS_ENV] = '1';
|
||||
const childExit = await runRespawnedAnalyze(childArgs, childEnv);
|
||||
if (childExit.status !== 0 || childExit.signal) {
|
||||
if (childProcessLikelyOom(childExit)) {
|
||||
cliError(
|
||||
` Analysis likely ran out of memory (heap cap auto-sized to ${RESPAWN_HEAP_MB}MB ≈ 0.75x RAM).\n` +
|
||||
` This repository's working set exceeds available RAM. Use a machine with more RAM,\n` +
|
||||
` or override the cap (a cap above physical RAM causes swap-thrash — use with care):\n` +
|
||||
` NODE_OPTIONS="--max-old-space-size=<MB>" gitnexus analyze [your-args]\n` +
|
||||
` (Windows: set NODE_OPTIONS=--max-old-space-size=<MB> && gitnexus analyze [your-args])\n` +
|
||||
` Analysis likely ran out of memory.\n` +
|
||||
` Retry with a larger heap if your machine allows it:\n` +
|
||||
` NODE_OPTIONS="--max-old-space-size=24576" gitnexus analyze [your-args]\n` +
|
||||
` (Windows: set NODE_OPTIONS=--max-old-space-size=24576 && gitnexus analyze [your-args])\n` +
|
||||
` If this persists, it may be a native crash unrelated to heap size.\n`,
|
||||
{ recoveryHint: 'heap-oom-respawn' },
|
||||
);
|
||||
@@ -525,7 +474,8 @@ async function ensureHeap(): Promise<boolean> {
|
||||
cliError(
|
||||
` Analysis aborted in a native worker or native binding path.\n` +
|
||||
` Try one of these recovery paths:\n` +
|
||||
` npm uninstall -g gitnexus && npm install -g gitnexus@latest (rebuilds native bindings)\n` +
|
||||
` gitnexus analyze --workers 0\n` +
|
||||
` npm uninstall -g gitnexus && npm install -g gitnexus@latest\n` +
|
||||
` Use Node 22 LTS if you are on a newer non-LTS runtime.\n`,
|
||||
{ recoveryHint: 'native-worker-abort' },
|
||||
);
|
||||
@@ -550,7 +500,6 @@ const ANALYZE_CLI_ENV_KEYS = [
|
||||
'GITNEXUS_VERBOSE',
|
||||
'GITNEXUS_PROFILE_DEFERRED',
|
||||
'GITNEXUS_PROFILE_DEFERRED_SLOW_MS',
|
||||
'GITNEXUS_DEBUG_HEAP',
|
||||
'GITNEXUS_MAX_FILE_SIZE',
|
||||
'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
|
||||
'GITNEXUS_WAL_CHECKPOINT_THRESHOLD',
|
||||
@@ -649,7 +598,7 @@ export interface AnalyzeOptions {
|
||||
workerTimeout?: string;
|
||||
/** Control LadybugDB WAL auto-checkpoint threshold during analyze. */
|
||||
walCheckpointThreshold?: string;
|
||||
/** Parse worker pool size (>=1); 0 is rejected (no sequential mode). */
|
||||
/** Parse worker pool size; 0 disables workers (sequential fallback). */
|
||||
workers?: string;
|
||||
embeddingThreads?: string;
|
||||
embeddingBatchSize?: string;
|
||||
@@ -844,11 +793,10 @@ const analyzeCommandImpl = async (
|
||||
let workerPoolSize: number | undefined;
|
||||
if (options.workers !== undefined) {
|
||||
const parsedWorkers = Number(options.workers);
|
||||
if (!Number.isInteger(parsedWorkers) || parsedWorkers < 1) {
|
||||
if (!Number.isInteger(parsedWorkers) || parsedWorkers < 0) {
|
||||
cliError(
|
||||
' --workers must be a positive integer (>= 1). ' +
|
||||
'GitNexus parses through a worker pool only — there is no sequential ' +
|
||||
'mode, so 0 is not allowed. Omit --workers for an auto-sized pool.\n',
|
||||
' --workers must be a non-negative integer. ' +
|
||||
'Pass 0 to disable the worker pool (sequential fallback).\n',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
@@ -943,13 +891,11 @@ const analyzeCommandImpl = async (
|
||||
}
|
||||
|
||||
// If the target repo contains files an optional grammar would parse but
|
||||
// that grammar's native binding is absent (or disabled via
|
||||
// GITNEXUS_SKIP_OPTIONAL_GRAMMARS), warn before analysis so users learn why
|
||||
// those files end up unparsed instead of silently getting a degraded index.
|
||||
// The extension set is derived from OPTIONAL_GRAMMARS so it can't drift.
|
||||
// that grammar's native binding is absent, warn before analysis so users
|
||||
// learn why those files end up unparsed instead of silently getting a
|
||||
// degraded index.
|
||||
try {
|
||||
const optionalGlobs = getOptionalGrammarExtensions().map((e) => `**/*${e}`);
|
||||
const matches = await glob(optionalGlobs, {
|
||||
const matches = await glob(['**/*.dart', '**/*.proto'], {
|
||||
cwd: repoPath,
|
||||
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
||||
dot: false,
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Editor targets — the single source of truth for *where* GitNexus writes its
|
||||
* per-editor configuration and *how* its entries are identified.
|
||||
*
|
||||
* `setup` (writes these) and `uninstall` (removes them) both consume this
|
||||
* module so the two stay structurally in lock-step: add or change a target
|
||||
* here and both sides follow. This is declarative metadata only — file
|
||||
* locations, JSON key paths, hook event names, command needles, and script
|
||||
* directories, plus the shared `detectIndentation` formatting helper. The
|
||||
* format-specific read/write logic (JSONC merge, TOML upsert, OpenCode's flat
|
||||
* command array, Gemini's hook schema) deliberately stays in setup.ts /
|
||||
* uninstall.ts.
|
||||
*
|
||||
* The `setup → uninstall` round-trip integration test verifies the two
|
||||
* implementations remain behaviourally symmetrical on top of this shared
|
||||
* structure.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
export type EditorId = 'cursor' | 'claude' | 'antigravity' | 'opencode' | 'codex';
|
||||
|
||||
/** An editor whose MCP config is a JSONC document (server keyed by name). */
|
||||
export interface McpJsoncTarget {
|
||||
id: EditorId;
|
||||
label: string;
|
||||
/** Absolute path to the editor's MCP config file. */
|
||||
file: string;
|
||||
/**
|
||||
* JSON path of the gitnexus server entry within that file. Typed as
|
||||
* `string[]` (all our keys are object keys) so it satisfies both setup's
|
||||
* `mergeJsoncFile(string[])` and uninstall's `removeJsoncKey(JSONPath)`
|
||||
* without either side needing a cast.
|
||||
*/
|
||||
keyPath: string[];
|
||||
}
|
||||
|
||||
/** Codex stores MCP config as a TOML table, not JSONC. */
|
||||
export interface CodexMcpTarget {
|
||||
id: 'codex';
|
||||
label: string;
|
||||
/** Absolute path to ~/.codex/config.toml. */
|
||||
configFile: string;
|
||||
/** The TOML table header (without brackets) setup writes / uninstall strips. */
|
||||
tomlSection: string;
|
||||
}
|
||||
|
||||
export interface SkillTarget {
|
||||
id: EditorId;
|
||||
label: string;
|
||||
/** Absolute path to the editor's skills directory. */
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export interface HookTarget {
|
||||
id: EditorId;
|
||||
label: string;
|
||||
/** Absolute path to the editor's settings file (JSONC). */
|
||||
settingsFile: string;
|
||||
/** Hook event arrays that may hold a gitnexus entry. */
|
||||
events: string[];
|
||||
/** Substring identifying the gitnexus command within a hook entry. */
|
||||
needle: string;
|
||||
/** Absolute path to the bundled hook-script directory setup writes. */
|
||||
scriptDir: string;
|
||||
}
|
||||
|
||||
export interface EditorTargets {
|
||||
/** JSONC-format MCP entries: Cursor, Claude Code, Antigravity, OpenCode. */
|
||||
mcpJsonc: McpJsoncTarget[];
|
||||
/** Codex MCP (TOML). */
|
||||
codex: CodexMcpTarget;
|
||||
/** Skill install directories, one per editor that supports skills. */
|
||||
skills: SkillTarget[];
|
||||
/** Hook registrations + their bundled script directories. */
|
||||
hooks: HookTarget[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all editor targets for the given home directory. Defaults to
|
||||
* `os.homedir()`; call sites pass it through so tests can point HOME at a temp
|
||||
* dir. Paths are computed at call time (not module load) so a test setting
|
||||
* `process.env.HOME` before invoking sees the right locations.
|
||||
*/
|
||||
export function getEditorTargets(home: string = os.homedir()): EditorTargets {
|
||||
const mcpJsonc: McpJsoncTarget[] = [
|
||||
{
|
||||
id: 'cursor',
|
||||
label: 'Cursor',
|
||||
file: path.join(home, '.cursor', 'mcp.json'),
|
||||
keyPath: ['mcpServers', 'gitnexus'],
|
||||
},
|
||||
{
|
||||
id: 'claude',
|
||||
label: 'Claude Code',
|
||||
file: path.join(home, '.claude.json'),
|
||||
keyPath: ['mcpServers', 'gitnexus'],
|
||||
},
|
||||
{
|
||||
id: 'antigravity',
|
||||
label: 'Antigravity',
|
||||
file: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
||||
keyPath: ['mcpServers', 'gitnexus'],
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
label: 'OpenCode',
|
||||
file: path.join(home, '.config', 'opencode', 'opencode.json'),
|
||||
// OpenCode nests servers under `mcp`, not `mcpServers`.
|
||||
keyPath: ['mcp', 'gitnexus'],
|
||||
},
|
||||
];
|
||||
|
||||
const codex: CodexMcpTarget = {
|
||||
id: 'codex',
|
||||
label: 'Codex',
|
||||
configFile: path.join(home, '.codex', 'config.toml'),
|
||||
tomlSection: 'mcp_servers.gitnexus',
|
||||
};
|
||||
|
||||
const skills: SkillTarget[] = [
|
||||
{ id: 'claude', label: 'Claude Code', dir: path.join(home, '.claude', 'skills') },
|
||||
{
|
||||
id: 'antigravity',
|
||||
label: 'Antigravity',
|
||||
dir: path.join(home, '.gemini', 'antigravity', 'skills'),
|
||||
},
|
||||
{ id: 'cursor', label: 'Cursor', dir: path.join(home, '.cursor', 'skills') },
|
||||
{ id: 'opencode', label: 'OpenCode', dir: path.join(home, '.config', 'opencode', 'skills') },
|
||||
// Codex reads skills from ~/.agents/skills (not ~/.codex).
|
||||
{ id: 'codex', label: 'Codex', dir: path.join(home, '.agents', 'skills') },
|
||||
];
|
||||
|
||||
const hooks: HookTarget[] = [
|
||||
{
|
||||
id: 'claude',
|
||||
label: 'Claude Code',
|
||||
settingsFile: path.join(home, '.claude', 'settings.json'),
|
||||
events: ['PreToolUse', 'PostToolUse'],
|
||||
needle: 'gitnexus-hook',
|
||||
scriptDir: path.join(home, '.claude', 'hooks', 'gitnexus'),
|
||||
},
|
||||
{
|
||||
id: 'antigravity',
|
||||
label: 'Antigravity',
|
||||
settingsFile: path.join(home, '.gemini', 'settings.json'),
|
||||
events: ['AfterTool'],
|
||||
needle: 'gitnexus-antigravity-hook',
|
||||
scriptDir: path.join(home, '.gemini', 'config', 'hooks', 'gitnexus'),
|
||||
},
|
||||
];
|
||||
|
||||
return { mcpJsonc, codex, skills, hooks };
|
||||
}
|
||||
|
||||
/** Look up a single JSONC MCP target by editor id (throws if unknown). */
|
||||
export function mcpTarget(id: EditorId, home?: string): McpJsoncTarget {
|
||||
const t = getEditorTargets(home).mcpJsonc.find((m) => m.id === id);
|
||||
if (!t) throw new Error(`No JSONC MCP target for editor "${id}"`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Look up a single skill target by editor id (throws if unknown). */
|
||||
export function skillTarget(id: EditorId, home?: string): SkillTarget {
|
||||
const t = getEditorTargets(home).skills.find((s) => s.id === id);
|
||||
if (!t) throw new Error(`No skill target for editor "${id}"`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Look up a single hook target by editor id (throws if unknown). */
|
||||
export function hookTarget(id: EditorId, home?: string): HookTarget {
|
||||
const t = getEditorTargets(home).hooks.find((h) => h.id === id);
|
||||
if (!t) throw new Error(`No hook target for editor "${id}"`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect indentation style from file content so JSONC edits preserve the file's
|
||||
* existing formatting. Shared by setup (writes) and uninstall (removes).
|
||||
*/
|
||||
export function detectIndentation(raw: string): { tabSize: number; insertSpaces: boolean } {
|
||||
const firstIndented = raw.match(/^( +|\t)/m);
|
||||
if (!firstIndented) return { tabSize: 2, insertSpaces: true };
|
||||
if (firstIndented[1] === '\t') return { tabSize: 1, insertSpaces: false };
|
||||
return { tabSize: firstIndented[1].length, insertSpaces: true };
|
||||
}
|
||||
@@ -32,11 +32,7 @@
|
||||
import http from 'http';
|
||||
import { isIPv4, isIPv6 } from 'node:net';
|
||||
import { writeSync } from 'node:fs';
|
||||
import {
|
||||
LocalBackend,
|
||||
type RepoListing,
|
||||
type ListReposPagination,
|
||||
} from '../mcp/local/local-backend.js';
|
||||
import { LocalBackend } from '../mcp/local/local-backend.js';
|
||||
import { logger } from '../core/logger.js';
|
||||
import { cliInfo, cliWarn, cliError } from './cli-message.js';
|
||||
import { formatDetectChangesResult } from './detect-changes-format.js';
|
||||
@@ -269,22 +265,13 @@ export function formatCypherResult(result: any): string {
|
||||
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
export function formatListReposResult(result: {
|
||||
repositories: RepoListing[];
|
||||
pagination?: ListReposPagination;
|
||||
}): string {
|
||||
// `list_repos` always returns the paginated { repositories, pagination } object (#2119).
|
||||
const repos = result.repositories;
|
||||
const pg = result.pagination;
|
||||
|
||||
if (repos.length === 0) {
|
||||
return pg && pg.total > 0
|
||||
? `No repositories on this page (offset ${pg.offset} of ${pg.total} total).`
|
||||
: 'No indexed repositories.';
|
||||
export function formatListReposResult(result: any): string {
|
||||
if (!Array.isArray(result) || result.length === 0) {
|
||||
return 'No indexed repositories.';
|
||||
}
|
||||
|
||||
const lines = ['Indexed repositories:\n'];
|
||||
for (const r of repos) {
|
||||
for (const r of result) {
|
||||
const stats = r.stats || {};
|
||||
lines.push(
|
||||
` ${r.name} — ${stats.nodes || '?'} symbols, ${stats.edges || '?'} relationships, ${stats.processes || '?'} flows`,
|
||||
@@ -292,13 +279,6 @@ export function formatListReposResult(result: {
|
||||
lines.push(` Path: ${r.path}`);
|
||||
lines.push(` Indexed: ${r.indexedAt}`);
|
||||
}
|
||||
if (pg) {
|
||||
lines.push('');
|
||||
lines.push(
|
||||
` Showing ${repos.length} of ${pg.total} (offset ${pg.offset}).` +
|
||||
(pg.hasMore ? ` More available — re-run with offset ${pg.nextOffset}.` : ''),
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -345,9 +325,6 @@ function getNextStepHint(toolName: string): string {
|
||||
case 'detect_changes':
|
||||
return '\n---\nNext: Run gitnexus-context "<symbol>" on high-risk changed symbols to check their callers.';
|
||||
|
||||
case 'list_repos':
|
||||
return '\n---\nNext: READ gitnexus://repo/{name}/context for a repo above. If pagination.hasMore is true, re-run list_repos with offset set to pagination.nextOffset to page through the rest.';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ const TITLE_KEYS = {
|
||||
const COMMAND_DESCRIPTION_KEYS = {
|
||||
'': 'help.description.root',
|
||||
setup: 'help.command.setup.description',
|
||||
uninstall: 'help.command.uninstall.description',
|
||||
analyze: 'help.command.analyze.description',
|
||||
index: 'help.command.index.description',
|
||||
serve: 'help.command.serve.description',
|
||||
@@ -70,7 +69,6 @@ const OPTION_DESCRIPTION_KEYS = {
|
||||
'index|--allow-non-git': 'help.option.index.allowNonGit',
|
||||
'serve|-p, --port <port>': 'help.option.port',
|
||||
'serve|--host <host>': 'help.option.serve.host',
|
||||
'uninstall|-f, --force': 'help.option.uninstall.force',
|
||||
'clean|-f, --force': 'help.option.force.confirmation',
|
||||
'clean|--all': 'help.option.clean.all',
|
||||
'clean|--lbug-sidecars': 'help.option.clean.lbugSidecars',
|
||||
|
||||
@@ -106,8 +106,6 @@ export const en = {
|
||||
'help.option.version': 'output the version number',
|
||||
'help.command.setup.description':
|
||||
'One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex',
|
||||
'help.command.uninstall.description':
|
||||
'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
|
||||
'help.command.analyze.description': 'Index a repository (full analysis)',
|
||||
'help.command.index.description':
|
||||
'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
|
||||
@@ -177,7 +175,7 @@ export const en = {
|
||||
'help.option.analyze.walCheckpointThreshold':
|
||||
'LadybugDB WAL auto-checkpoint threshold in bytes during analyze (integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).',
|
||||
'help.option.analyze.workers':
|
||||
'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.',
|
||||
'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).',
|
||||
'help.option.analyze.embeddingThreads': 'Limit local ONNX embedding CPU threads',
|
||||
'help.option.analyze.embeddingBatchSize': 'Number of nodes per embedding batch',
|
||||
'help.option.analyze.embeddingSubBatchSize': 'Number of chunks per embedding model call',
|
||||
@@ -187,12 +185,11 @@ export const en = {
|
||||
'help.option.port': 'Port number',
|
||||
'help.option.serve.host': 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)',
|
||||
'help.option.force.confirmation': 'Skip confirmation prompt',
|
||||
'help.option.uninstall.force': 'Apply the changes (default is a dry-run preview)',
|
||||
'help.option.clean.all': 'Clean all indexed repos',
|
||||
'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
|
||||
'help.option.wiki.force': 'Force full regeneration even if up to date',
|
||||
'help.option.wiki.provider':
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
|
||||
'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
||||
|
||||
@@ -108,8 +108,6 @@ export const zhCN = {
|
||||
'help.option.help': '显示命令帮助',
|
||||
'help.option.version': '输出版本号',
|
||||
'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、OpenCode、Codex 配置 MCP',
|
||||
'help.command.uninstall.description':
|
||||
'撤销 `setup`:从所有检测到的编辑器中移除 GitNexus 的 MCP 配置、技能和钩子',
|
||||
'help.command.analyze.description': '索引仓库(完整分析)',
|
||||
'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)',
|
||||
'help.command.serve.description': '启动供 Web UI 连接的本地 HTTP 服务器',
|
||||
@@ -166,7 +164,7 @@ export const zhCN = {
|
||||
'help.option.analyze.walCheckpointThreshold':
|
||||
'analyze 期间 LadybugDB WAL 自动 checkpoint 阈值(字节,整数 >= -1;默认:67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。',
|
||||
'help.option.analyze.workers':
|
||||
'解析 worker 池大小(>=1)。默认:cores-1,最多 16,按仓库规模自适应。',
|
||||
'解析 worker 池大小。默认:cores-1,最多 16。传 0 禁用 worker(顺序执行)。',
|
||||
'help.option.analyze.embeddingThreads': '限制本地 ONNX 嵌入 CPU 线程数',
|
||||
'help.option.analyze.embeddingBatchSize': '每个嵌入批次的节点数',
|
||||
'help.option.analyze.embeddingSubBatchSize': '每次嵌入模型调用的分块数',
|
||||
@@ -176,12 +174,11 @@ export const zhCN = {
|
||||
'help.option.port': '端口号',
|
||||
'help.option.serve.host': '绑定地址(默认:127.0.0.1;远程访问可用 0.0.0.0)',
|
||||
'help.option.force.confirmation': '跳过确认提示',
|
||||
'help.option.uninstall.force': '应用更改(默认仅为预演预览)',
|
||||
'help.option.clean.all': '清理所有已索引仓库',
|
||||
'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
|
||||
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
|
||||
'help.option.wiki.provider':
|
||||
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
|
||||
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude 或 codex(默认:openai)',
|
||||
'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
|
||||
|
||||
@@ -23,14 +23,6 @@ program
|
||||
)
|
||||
.action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
|
||||
|
||||
program
|
||||
.command('uninstall')
|
||||
.description(
|
||||
'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
|
||||
)
|
||||
.option('-f, --force', 'Apply the changes (default is a dry-run preview)')
|
||||
.action(createLazyAction(() => import('./uninstall.js'), 'uninstallCommand'));
|
||||
|
||||
program
|
||||
.command('analyze [path]')
|
||||
.description('Index a repository (full analysis)')
|
||||
@@ -95,7 +87,7 @@ program
|
||||
)
|
||||
.option(
|
||||
'--workers <n>',
|
||||
'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.',
|
||||
'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).',
|
||||
)
|
||||
.option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
|
||||
.option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
|
||||
@@ -163,7 +155,7 @@ program
|
||||
.option('-f, --force', 'Force full regeneration even if up to date')
|
||||
.option(
|
||||
'--provider <provider>',
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
|
||||
)
|
||||
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
|
||||
.option(
|
||||
|
||||
@@ -4,22 +4,18 @@
|
||||
* tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
|
||||
* under vendor/ and materialized into node_modules/ at postinstall. Dart
|
||||
* and Proto are built from source with node-gyp; Swift ships platform
|
||||
* prebuilds activated via node-gyp-build. tree-sitter-kotlin is a declared
|
||||
* optionalDependency (not vendored). All can be skipped via
|
||||
* prebuilds activated via node-gyp-build. All three can be skipped via
|
||||
* GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
|
||||
* soft-fail when the toolchain is missing (Dart/Proto), when no prebuild
|
||||
* matches the host platform (Swift), or when the optional install was
|
||||
* skipped or its native build failed (Kotlin).
|
||||
* soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
|
||||
* matches the host platform (Swift).
|
||||
*
|
||||
* Either path produces the same observable: the .node binding is absent
|
||||
* at runtime. This helper detects that condition and surfaces a single
|
||||
* stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt
|
||||
* stderr line per missing grammar so users learn why .dart/.proto/.swift
|
||||
* support is unavailable instead of silently getting a degraded index.
|
||||
*/
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { isGrammarRuntimeSkipped } from '../core/tree-sitter/parser-loader.js';
|
||||
import { cliWarn } from './cli-message.js';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
@@ -31,55 +27,17 @@ interface OptionalGrammar {
|
||||
pkg: string;
|
||||
/** File extensions this grammar parses */
|
||||
extensions: string[];
|
||||
/**
|
||||
* SupportedLanguages id, when this grammar backs an ingestion language.
|
||||
* Used to ask `isGrammarRuntimeSkipped` whether the grammar was disabled via
|
||||
* `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` (vs. genuinely missing). Omitted for
|
||||
* `.proto`, which is a gRPC-extractor concern, not a SupportedLanguages.
|
||||
*/
|
||||
language?: SupportedLanguages;
|
||||
}
|
||||
|
||||
const OPTIONAL_GRAMMARS: OptionalGrammar[] = [
|
||||
{
|
||||
name: 'tree-sitter-dart',
|
||||
pkg: 'tree-sitter-dart',
|
||||
extensions: ['.dart'],
|
||||
language: SupportedLanguages.Dart,
|
||||
},
|
||||
{ name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
|
||||
{ name: 'tree-sitter-proto', pkg: 'tree-sitter-proto', extensions: ['.proto'] },
|
||||
{
|
||||
name: 'tree-sitter-swift',
|
||||
pkg: 'tree-sitter-swift',
|
||||
extensions: ['.swift'],
|
||||
language: SupportedLanguages.Swift,
|
||||
},
|
||||
{
|
||||
name: 'tree-sitter-kotlin',
|
||||
pkg: 'tree-sitter-kotlin',
|
||||
extensions: ['.kt', '.kts'],
|
||||
language: SupportedLanguages.Kotlin,
|
||||
},
|
||||
{ name: 'tree-sitter-swift', pkg: 'tree-sitter-swift', extensions: ['.swift'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* The file extensions backed by an optional grammar — the single source for
|
||||
* the `analyze` preflight glob (so the glob can't drift from this list).
|
||||
*/
|
||||
export function getOptionalGrammarExtensions(): string[] {
|
||||
return [...new Set(OPTIONAL_GRAMMARS.flatMap((g) => g.extensions))];
|
||||
}
|
||||
|
||||
export interface MissingGrammar {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
/**
|
||||
* `missing` — the native binding could not be loaded (not installed / build
|
||||
* soft-failed / no prebuild). `skipped` — the binding is fine but the user
|
||||
* disabled it via `GITNEXUS_SKIP_OPTIONAL_GRAMMARS`. Drives the warning text
|
||||
* so a deliberate opt-out is not told to reinstall.
|
||||
*/
|
||||
reason: 'missing' | 'skipped';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,13 +59,6 @@ export interface MissingGrammar {
|
||||
export function detectMissingOptionalGrammars(): MissingGrammar[] {
|
||||
const missing: MissingGrammar[] = [];
|
||||
for (const g of OPTIONAL_GRAMMARS) {
|
||||
// Deliberate runtime opt-out comes first: even an installed binding is
|
||||
// treated as unavailable, with a `skipped` reason so the warning says so
|
||||
// instead of suggesting a reinstall (#2101 review).
|
||||
if (g.language !== undefined && isGrammarRuntimeSkipped(g.language)) {
|
||||
missing.push({ name: g.name, extensions: g.extensions, reason: 'skipped' });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
_require(g.pkg);
|
||||
} catch (err) {
|
||||
@@ -129,7 +80,7 @@ export function detectMissingOptionalGrammars(): MissingGrammar[] {
|
||||
{ grammar: g.name, extensions: g.extensions, error: msg },
|
||||
);
|
||||
}
|
||||
missing.push({ name: g.name, extensions: g.extensions, reason: 'missing' });
|
||||
missing.push({ name: g.name, extensions: g.extensions });
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
@@ -159,16 +110,9 @@ export function warnMissingOptionalGrammars(opts?: {
|
||||
if (relevantExtensions && !g.extensions.some((e) => relevantExtensions.has(e))) {
|
||||
continue;
|
||||
}
|
||||
const exts = g.extensions.join('/');
|
||||
const message =
|
||||
g.reason === 'skipped'
|
||||
? `GitNexus${ctx}: optional grammar "${g.name}" is disabled via GITNEXUS_SKIP_OPTIONAL_GRAMMARS — ${exts} files will not be parsed. Unset the variable to re-enable.`
|
||||
: `GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${exts} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`;
|
||||
cliWarn(message, {
|
||||
grammar: g.name,
|
||||
extensions: g.extensions,
|
||||
reason: g.reason,
|
||||
context: opts?.context,
|
||||
});
|
||||
cliWarn(
|
||||
`GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${g.extensions.join('/')} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`,
|
||||
{ grammar: g.name, extensions: g.extensions, context: opts?.context },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-32
@@ -15,13 +15,6 @@ import { promisify } from 'util';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { parseTree, modify, applyEdits, ParseError, parse as parseJsonc } from 'jsonc-parser';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
import {
|
||||
getEditorTargets,
|
||||
mcpTarget,
|
||||
skillTarget,
|
||||
hookTarget,
|
||||
detectIndentation,
|
||||
} from './editor-targets.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -169,6 +162,17 @@ function getOpenCodeMcpEntry() {
|
||||
return { type: 'local', command: ['npx', '-y', MCP_PINNED_REF, 'mcp'] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect indentation style from file content.
|
||||
* Returns formatting options matching the file's existing style.
|
||||
*/
|
||||
function detectIndentation(raw: string): { tabSize: number; insertSpaces: boolean } {
|
||||
const firstIndented = raw.match(/^( +|\t)/m);
|
||||
if (!firstIndented) return { tabSize: 2, insertSpaces: true };
|
||||
if (firstIndented[1] === '\t') return { tabSize: 1, insertSpaces: false };
|
||||
return { tabSize: firstIndented[1].length, insertSpaces: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a key/value pair into a JSONC config file, preserving comments and formatting.
|
||||
* If the file is genuinely corrupt (not valid JSONC), leaves it untouched.
|
||||
@@ -229,9 +233,9 @@ async function setupCursor(result: SetupResult): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { file: mcpPath, keyPath } = mcpTarget('cursor');
|
||||
const mcpPath = path.join(cursorDir, 'mcp.json');
|
||||
try {
|
||||
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
||||
const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
|
||||
if (ok) {
|
||||
result.configured.push('Cursor');
|
||||
} else {
|
||||
@@ -250,9 +254,9 @@ async function setupClaudeCode(result: SetupResult): Promise<void> {
|
||||
}
|
||||
|
||||
// Claude Code stores MCP config in ~/.claude.json
|
||||
const { file: mcpPath, keyPath } = mcpTarget('claude');
|
||||
const mcpPath = path.join(os.homedir(), '.claude.json');
|
||||
try {
|
||||
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
||||
const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
|
||||
if (ok) {
|
||||
result.configured.push('Claude Code');
|
||||
} else {
|
||||
@@ -272,7 +276,7 @@ async function installClaudeCodeSkills(result: SetupResult): Promise<void> {
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
if (!(await dirExists(claudeDir))) return;
|
||||
|
||||
const skillsDir = skillTarget('claude').dir;
|
||||
const skillsDir = path.join(claudeDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
@@ -418,14 +422,13 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
if (!(await dirExists(claudeDir))) return;
|
||||
|
||||
const claudeHook = hookTarget('claude');
|
||||
const settingsPath = claudeHook.settingsFile;
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
|
||||
// Source hooks bundled within the gitnexus package (hooks/claude/)
|
||||
const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
|
||||
|
||||
// Copy unified hook script to ~/.claude/hooks/gitnexus/
|
||||
const destHooksDir = claudeHook.scriptDir;
|
||||
const destHooksDir = path.join(claudeDir, 'hooks', 'gitnexus');
|
||||
|
||||
try {
|
||||
await fs.mkdir(destHooksDir, { recursive: true });
|
||||
@@ -491,7 +494,7 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
||||
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
|
||||
// Session context is delivered via CLAUDE.md / skills instead.
|
||||
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', claudeHook.needle)) {
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse')) {
|
||||
hookEntries.push({
|
||||
eventName: 'PreToolUse',
|
||||
value: {
|
||||
@@ -507,7 +510,7 @@ async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', claudeHook.needle)) {
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse')) {
|
||||
hookEntries.push({
|
||||
eventName: 'PostToolUse',
|
||||
value: {
|
||||
@@ -563,9 +566,9 @@ async function setupAntigravity(result: SetupResult): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { file: mcpPath, keyPath } = mcpTarget('antigravity');
|
||||
const mcpPath = path.join(antigravityDir, 'mcp_config.json');
|
||||
try {
|
||||
const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
|
||||
const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], getMcpEntry());
|
||||
if (ok) {
|
||||
result.configured.push('Antigravity');
|
||||
} else {
|
||||
@@ -587,7 +590,7 @@ async function installAntigravitySkills(result: SetupResult): Promise<void> {
|
||||
const antigravityDir = path.join(os.homedir(), '.gemini', 'antigravity');
|
||||
if (!(await dirExists(antigravityDir))) return;
|
||||
|
||||
const skillsDir = skillTarget('antigravity').dir;
|
||||
const skillsDir = path.join(antigravityDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
@@ -615,9 +618,9 @@ async function installAntigravityHooks(result: SetupResult): Promise<void> {
|
||||
const antigravityDir = path.join(os.homedir(), '.gemini', 'antigravity');
|
||||
if (!(await dirExists(antigravityDir))) return;
|
||||
|
||||
const antigravityHook = hookTarget('antigravity');
|
||||
const settingsPath = antigravityHook.settingsFile;
|
||||
const destHooksDir = antigravityHook.scriptDir;
|
||||
const geminiDir = path.join(os.homedir(), '.gemini');
|
||||
const settingsPath = path.join(geminiDir, 'settings.json');
|
||||
const destHooksDir = path.join(geminiDir, 'config', 'hooks', 'gitnexus');
|
||||
|
||||
// The antigravity adapter shares its lock/probe helpers with the claude
|
||||
// adapter — same DB, same concurrency rules — so we reuse those CJS files
|
||||
@@ -691,7 +694,7 @@ async function installAntigravityHooks(result: SetupResult): Promise<void> {
|
||||
|
||||
const hookEntries: Array<{ eventName: string; value: unknown }> = [];
|
||||
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'AfterTool', antigravityHook.needle)) {
|
||||
if (!hasGitnexusHook(parsed?.hooks, 'AfterTool', 'gitnexus-antigravity-hook')) {
|
||||
// Matcher follows the Gemini CLI built-in tool naming (snake_case).
|
||||
// search_file_content / glob cover content + filename search; run_shell_command
|
||||
// catches rg/grep invocations and the git commit family for stale-index hints.
|
||||
@@ -739,9 +742,9 @@ async function setupOpenCode(result: SetupResult): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { file: configPath, keyPath } = mcpTarget('opencode');
|
||||
const configPath = path.join(opencodeDir, 'opencode.json');
|
||||
try {
|
||||
const ok = await mergeJsoncFile(configPath, keyPath, getOpenCodeMcpEntry());
|
||||
const ok = await mergeJsoncFile(configPath, ['mcp', 'gitnexus'], getOpenCodeMcpEntry());
|
||||
if (ok) {
|
||||
result.configured.push('OpenCode');
|
||||
} else {
|
||||
@@ -761,7 +764,7 @@ function getCodexMcpTomlSection(): string {
|
||||
const entry = getMcpEntry();
|
||||
const command = JSON.stringify(entry.command);
|
||||
const args = `[${entry.args.map((arg) => JSON.stringify(arg)).join(', ')}]`;
|
||||
return `[${getEditorTargets().codex.tomlSection}]\ncommand = ${command}\nargs = ${args}\n`;
|
||||
return `[mcp_servers.gitnexus]\ncommand = ${command}\nargs = ${args}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -775,7 +778,7 @@ async function upsertCodexConfigToml(configPath: string): Promise<void> {
|
||||
existing = '';
|
||||
}
|
||||
|
||||
if (existing.includes(`[${getEditorTargets().codex.tomlSection}]`)) {
|
||||
if (existing.includes('[mcp_servers.gitnexus]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -806,7 +809,7 @@ async function setupCodex(result: SetupResult): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
const configPath = getEditorTargets().codex.configFile;
|
||||
const configPath = path.join(codexDir, 'config.toml');
|
||||
await upsertCodexConfigToml(configPath);
|
||||
result.configured.push('Codex (MCP added to ~/.codex/config.toml)');
|
||||
} catch (err: any) {
|
||||
@@ -917,7 +920,7 @@ async function installCursorSkills(result: SetupResult): Promise<void> {
|
||||
const cursorDir = path.join(os.homedir(), '.cursor');
|
||||
if (!(await dirExists(cursorDir))) return;
|
||||
|
||||
const skillsDir = skillTarget('cursor').dir;
|
||||
const skillsDir = path.join(cursorDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
@@ -935,7 +938,7 @@ async function installOpenCodeSkills(result: SetupResult): Promise<void> {
|
||||
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
||||
if (!(await dirExists(opencodeDir))) return;
|
||||
|
||||
const skillsDir = skillTarget('opencode').dir;
|
||||
const skillsDir = path.join(opencodeDir, 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
@@ -955,7 +958,7 @@ async function installCodexSkills(result: SetupResult): Promise<void> {
|
||||
const codexDir = path.join(os.homedir(), '.codex');
|
||||
if (!(await dirExists(codexDir))) return;
|
||||
|
||||
const skillsDir = skillTarget('codex').dir;
|
||||
const skillsDir = path.join(os.homedir(), '.agents', 'skills');
|
||||
try {
|
||||
const installed = await installSkillsTo(skillsDir);
|
||||
if (installed.length > 0) {
|
||||
|
||||
@@ -649,9 +649,9 @@ const renderSkillMarkdown = (
|
||||
: community.label;
|
||||
lines.push('## How to Explore');
|
||||
lines.push('');
|
||||
lines.push(`1. \`context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
|
||||
lines.push(`1. \`gitnexus_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
|
||||
lines.push(
|
||||
`2. \`query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
|
||||
`2. \`gitnexus_query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
|
||||
);
|
||||
lines.push('3. Read key files listed above for implementation details');
|
||||
lines.push('');
|
||||
|
||||
@@ -1,518 +0,0 @@
|
||||
/**
|
||||
* Uninstall Command
|
||||
*
|
||||
* Reverses `gitnexus setup`: removes the GitNexus MCP server entries,
|
||||
* skills, and hooks that setup writes into each detected AI editor's
|
||||
* global configuration. The set of targets (paths, key paths, hook events,
|
||||
* needles, script dirs) is shared with setup.ts via editor-targets.ts, so the
|
||||
* two stay in lock-step.
|
||||
*
|
||||
* Surgical and idempotent: only gitnexus-owned keys/entries/dirs are
|
||||
* removed. Unrelated user config (other MCP servers, other hooks, JSONC
|
||||
* comments, indentation) is preserved. Files that are absent or that
|
||||
* never contained a gitnexus entry are left untouched.
|
||||
*
|
||||
* Ownership is by name: skill directories are matched by the bundled gitnexus
|
||||
* skill names, MCP entries by the `gitnexus` key, hooks by the gitnexus command
|
||||
* needle. There is no per-install provenance marker yet (a user dir that
|
||||
* happens to share a bundled skill name, or files a user added inside an
|
||||
* installed skill dir, are matched purely by name) — which is why uninstall is
|
||||
* a dry-run preview by default and prints the exact paths it will remove.
|
||||
* Richer provenance tracking is a tracked follow-up.
|
||||
*
|
||||
* Intentionally NOT done here (printed as hints instead, since both are
|
||||
* destructive in ways setup never caused):
|
||||
* - per-repo indexes → `gitnexus clean --all`
|
||||
* - the global npm package → `npm uninstall -g gitnexus`
|
||||
*
|
||||
* Default is a dry-run preview; pass --force to apply.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
parseTree,
|
||||
modify,
|
||||
applyEdits,
|
||||
findNodeAtLocation,
|
||||
parse as parseJsonc,
|
||||
type ParseError,
|
||||
type JSONPath,
|
||||
} from 'jsonc-parser';
|
||||
import { getEditorTargets, detectIndentation } from './editor-targets.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
interface UninstallResult {
|
||||
removed: string[];
|
||||
skipped: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
type RemovalStatus = 'removed' | 'absent' | 'corrupt' | 'missing';
|
||||
|
||||
/**
|
||||
* Remove a single key (by JSON path) from a JSONC file, preserving the
|
||||
* surrounding comments and formatting. Returns:
|
||||
* - 'missing': file does not exist
|
||||
* - 'absent': file exists but the key isn't there (nothing to do)
|
||||
* - 'corrupt': file isn't valid JSONC — left untouched on purpose
|
||||
* - 'removed': the key was present (and removed unless dryRun)
|
||||
*/
|
||||
async function removeJsoncKey(
|
||||
filePath: string,
|
||||
keyPath: JSONPath,
|
||||
dryRun: boolean,
|
||||
): Promise<RemovalStatus> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(filePath, 'utf-8');
|
||||
} catch {
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
if (raw.trim().length === 0) return 'absent';
|
||||
|
||||
const parseErrors: ParseError[] = [];
|
||||
const tree = parseTree(raw, parseErrors);
|
||||
if (!tree || tree.type !== 'object' || parseErrors.length > 0) return 'corrupt';
|
||||
|
||||
if (!findNodeAtLocation(tree, keyPath)) return 'absent';
|
||||
|
||||
if (!dryRun) {
|
||||
const formattingOptions = detectIndentation(raw);
|
||||
const edits = modify(raw, keyPath, undefined, { formattingOptions });
|
||||
await fs.writeFile(filePath, applyEdits(raw, edits), 'utf-8');
|
||||
}
|
||||
return 'removed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the gitnexus hook command(s) — those whose command string contains
|
||||
* `commandNeedle` — from the given `eventNames` arrays in a JSONC settings
|
||||
* file. Mirrors the idempotency probes in setup.ts (hasGitnexusHook /
|
||||
* geminiHasGitnexusHook). Returns how many event entries contained a gitnexus
|
||||
* command.
|
||||
*
|
||||
* Removal is element-granular to honor the "other hooks are preserved"
|
||||
* contract: only the matching command object inside an entry's `hooks[]` is
|
||||
* deleted. The surrounding matcher entry is removed only when it becomes
|
||||
* empty (i.e. it held nothing but gitnexus commands — which is exactly what
|
||||
* setup creates). A user who hand-added their own command alongside ours
|
||||
* keeps it. Edits are applied highest-index-first so earlier indices stay
|
||||
* valid across edits.
|
||||
*/
|
||||
async function removeHookEntries(
|
||||
filePath: string,
|
||||
eventNames: string[],
|
||||
commandNeedle: string,
|
||||
dryRun: boolean,
|
||||
): Promise<{ status: RemovalStatus; count: number }> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(filePath, 'utf-8');
|
||||
} catch {
|
||||
return { status: 'missing', count: 0 };
|
||||
}
|
||||
|
||||
if (raw.trim().length === 0) return { status: 'absent', count: 0 };
|
||||
|
||||
const parseErrors: ParseError[] = [];
|
||||
const tree = parseTree(raw, parseErrors);
|
||||
if (!tree || tree.type !== 'object' || parseErrors.length > 0) {
|
||||
return { status: 'corrupt', count: 0 };
|
||||
}
|
||||
|
||||
const parsed = parseJsonc(raw);
|
||||
const formattingOptions = detectIndentation(raw);
|
||||
let current = raw;
|
||||
let total = 0;
|
||||
|
||||
const isGitnexusHook = (hh: any): boolean =>
|
||||
typeof hh?.command === 'string' && hh.command.includes(commandNeedle);
|
||||
|
||||
for (const eventName of eventNames) {
|
||||
const entries = parsed?.hooks?.[eventName];
|
||||
if (!Array.isArray(entries)) continue;
|
||||
|
||||
// Walk entries high → low so removing a later one never shifts the
|
||||
// index of an earlier one.
|
||||
for (let entryIdx = entries.length - 1; entryIdx >= 0; entryIdx--) {
|
||||
const entry = entries[entryIdx];
|
||||
if (!Array.isArray(entry?.hooks)) continue;
|
||||
|
||||
const hookIdxs: number[] = [];
|
||||
entry.hooks.forEach((hh: any, hi: number) => {
|
||||
if (isGitnexusHook(hh)) hookIdxs.push(hi);
|
||||
});
|
||||
if (hookIdxs.length === 0) continue;
|
||||
|
||||
total += 1;
|
||||
if (dryRun) continue;
|
||||
|
||||
if (hookIdxs.length === entry.hooks.length) {
|
||||
// The entry held only gitnexus command(s) — drop the whole entry.
|
||||
const edits = modify(current, ['hooks', eventName, entryIdx], undefined, {
|
||||
formattingOptions,
|
||||
});
|
||||
current = applyEdits(current, edits);
|
||||
} else {
|
||||
// The entry also holds user command(s) — delete only ours, keep
|
||||
// the rest. Highest hook index first to keep lower indices valid.
|
||||
for (const hi of hookIdxs.reverse()) {
|
||||
const edits = modify(current, ['hooks', eventName, entryIdx, 'hooks', hi], undefined, {
|
||||
formattingOptions,
|
||||
});
|
||||
current = applyEdits(current, edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (total === 0) return { status: 'absent', count: 0 };
|
||||
if (!dryRun) await fs.writeFile(filePath, current, 'utf-8');
|
||||
return { status: 'removed', count: total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a directory tree if it exists. Returns true when something was
|
||||
* (or would be) removed.
|
||||
*/
|
||||
async function removeDir(dirPath: string, dryRun: boolean): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(dirPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!dryRun) await fs.rm(dirPath, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The exact set of skill directory names setup installs, derived from the
|
||||
* bundled `skills/` source the same way installSkillsTo does (flat
|
||||
* `{name}.md` and `{name}/SKILL.md` layouts). Deriving the set — rather
|
||||
* than globbing `gitnexus-*` — ensures we never delete a user's own
|
||||
* similarly-named skill folder.
|
||||
*/
|
||||
async function listGitnexusSkillNames(): Promise<string[]> {
|
||||
const skillsRoot =
|
||||
process.env.GITNEXUS_TEST_SKILLS_ROOT ?? path.join(__dirname, '..', '..', 'skills');
|
||||
|
||||
const names = new Set<string>();
|
||||
try {
|
||||
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
// Guard against a bare `.md` file: basename('.md', '.md') === '',
|
||||
// which would later resolve to the skills dir itself and wipe it.
|
||||
const base = path.basename(entry.name, '.md');
|
||||
if (base) names.add(base);
|
||||
} else if (entry.isDirectory()) {
|
||||
try {
|
||||
await fs.access(path.join(skillsRoot, entry.name, 'SKILL.md'));
|
||||
names.add(entry.name);
|
||||
} catch {
|
||||
// Not a skill directory — skip.
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return [...names];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the gitnexus skill directories from a target skills folder. Returns
|
||||
* the absolute paths that were removed (or would be removed in dryRun) so the
|
||||
* caller can show the user exactly what is affected.
|
||||
*/
|
||||
async function removeSkillsFrom(
|
||||
targetDir: string,
|
||||
skillNames: string[],
|
||||
dryRun: boolean,
|
||||
): Promise<string[]> {
|
||||
const removed: string[] = [];
|
||||
for (const name of skillNames) {
|
||||
// Defense in depth: an empty/relative/absolute name would resolve back to
|
||||
// targetDir (or escape it) and wipe unrelated content. Only act on a
|
||||
// plain child directory name.
|
||||
if (
|
||||
!name ||
|
||||
name.includes('/') ||
|
||||
name.includes('\\') ||
|
||||
name === '.' ||
|
||||
name === '..' ||
|
||||
path.isAbsolute(name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const dir = path.join(targetDir, name);
|
||||
if (await removeDir(dir, dryRun)) removed.push(dir);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the `[mcp_servers.gitnexus]` table — and any of its descendant
|
||||
* sub-tables (`[mcp_servers.gitnexus.env]`, `[[mcp_servers.gitnexus.x]]`) —
|
||||
* from Codex's config.toml. Used only as a fallback when the `codex` binary
|
||||
* isn't on PATH; the CLI's `codex mcp remove` is preferred.
|
||||
*
|
||||
* Hand-rolled (no TOML dependency), but careful about the cases a naive
|
||||
* line-scan gets wrong:
|
||||
* - descendant sub-tables of the section are also removed (else they'd be
|
||||
* left dangling, referencing a server that no longer exists);
|
||||
* - `[...]`-shaped lines inside a multiline string (`"""`/`'''`) are NOT
|
||||
* treated as table headers;
|
||||
* - unrelated whitespace/formatting elsewhere in the file is left intact
|
||||
* (no global blank-line reflow). Only a single blank separator line
|
||||
* directly above the removed section is dropped.
|
||||
*/
|
||||
function stripTomlSection(raw: string, sectionName: string): string {
|
||||
const header = `[${sectionName}]`;
|
||||
const childTable = `[${sectionName}.`;
|
||||
const childArray = `[[${sectionName}.`;
|
||||
// Capture group 1 is the bracket token only, so a trailing inline comment
|
||||
// (`[mcp_servers.gitnexus] # note`) is stripped before classification —
|
||||
// otherwise an exact `=== header` check fails and the section is left behind.
|
||||
const headerRe = /^(\[\[?[^[\]]+\]\]?)\s*(#.*)?$/;
|
||||
|
||||
const isSectionHeader = (token: string): boolean =>
|
||||
token === header || token.startsWith(childTable) || token.startsWith(childArray);
|
||||
|
||||
// Return the multiline-string delimiter still OPEN at the end of `line`,
|
||||
// given the state at its start (null = outside any multiline string). Scans
|
||||
// left→right so the delimiter that actually opens first wins — a line with an
|
||||
// odd count of BOTH `"""` and `'''` (e.g. `x = '''has """ inside`) no longer
|
||||
// mis-picks the wrong delimiter and desyncs the scanner.
|
||||
const multilineStateAfter = (line: string, startState: string | null): string | null => {
|
||||
let state = startState;
|
||||
let i = 0;
|
||||
while (i < line.length) {
|
||||
if (state) {
|
||||
const close = line.indexOf(state, i);
|
||||
if (close === -1) return state; // still open at end of line
|
||||
i = close + state.length;
|
||||
state = null;
|
||||
} else {
|
||||
const a = line.indexOf('"""', i);
|
||||
const b = line.indexOf("'''", i);
|
||||
if (a === -1 && b === -1) return null;
|
||||
const useA = b === -1 || (a !== -1 && a < b);
|
||||
state = useA ? '"""' : "'''";
|
||||
i = (useA ? a : b) + 3;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const out: string[] = [];
|
||||
let skipping = false;
|
||||
let mlDelim: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (mlDelim) {
|
||||
// Inside a multiline string: brackets here are data, not headers.
|
||||
mlDelim = multilineStateAfter(line, mlDelim);
|
||||
if (!skipping) out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmed = line.trim();
|
||||
const headerMatch = trimmed.match(headerRe);
|
||||
if (headerMatch) {
|
||||
if (isSectionHeader(headerMatch[1])) {
|
||||
// Drop a single blank separator line immediately above the section.
|
||||
if (!skipping && out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
||||
skipping = true;
|
||||
continue;
|
||||
}
|
||||
// A non-descendant header ends the section.
|
||||
skipping = false;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track whether this (non-header) line opens a multiline string so a
|
||||
// bracketed line inside it isn't mistaken for a header.
|
||||
mlDelim = multilineStateAfter(line, null);
|
||||
|
||||
if (!skipping) out.push(line);
|
||||
}
|
||||
|
||||
// Preserve the file's line endings: a CRLF (Windows) config.toml should not
|
||||
// be silently rewritten to LF. Rejoin with the dominant EOL of the input.
|
||||
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
||||
let result = out.join(eol);
|
||||
if (!result.endsWith(eol)) result += eol;
|
||||
return result;
|
||||
}
|
||||
|
||||
async function uninstallCodex(
|
||||
result: UninstallResult,
|
||||
dryRun: boolean,
|
||||
configPath: string,
|
||||
tomlSection: string,
|
||||
): Promise<void> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(configPath, 'utf-8');
|
||||
} catch {
|
||||
result.skipped.push('Codex MCP (not configured)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!raw.includes(`[${tomlSection}]`)) {
|
||||
result.skipped.push('Codex MCP (not configured)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
result.removed.push(`Codex MCP server — [${tomlSection}] in ${configPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the official CLI (mirrors setup's `codex mcp add`); fall back
|
||||
// to editing config.toml directly when the binary isn't on PATH.
|
||||
try {
|
||||
await execFileAsync('codex', ['mcp', 'remove', 'gitnexus'], {
|
||||
shell: process.platform === 'win32',
|
||||
windowsHide: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
result.removed.push("Codex MCP server — via 'codex mcp remove gitnexus'");
|
||||
return;
|
||||
} catch {
|
||||
// Fall through to manual edit.
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(configPath, stripTomlSection(raw, tomlSection), 'utf-8');
|
||||
result.removed.push(`Codex MCP server — [${tomlSection}] in ${configPath}`);
|
||||
} catch (err: any) {
|
||||
result.errors.push(`Codex: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main command ──────────────────────────────────────────────────
|
||||
|
||||
export const uninstallCommand = async (options?: { force?: boolean }) => {
|
||||
const dryRun = !options?.force;
|
||||
const targets = getEditorTargets();
|
||||
|
||||
console.log('');
|
||||
console.log(' GitNexus Uninstall');
|
||||
console.log(' ==================');
|
||||
console.log('');
|
||||
if (dryRun) {
|
||||
console.log(' Dry run — nothing will be changed. Re-run with --force to apply.');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const result: UninstallResult = { removed: [], skipped: [], errors: [] };
|
||||
|
||||
// ─── MCP server entries (JSONC editors) ──────────────────────────
|
||||
for (const target of targets.mcpJsonc) {
|
||||
try {
|
||||
const status = await removeJsoncKey(target.file, target.keyPath, dryRun);
|
||||
if (status === 'removed')
|
||||
result.removed.push(
|
||||
`${target.label} MCP server — ${target.keyPath.join('.')} in ${target.file}`,
|
||||
);
|
||||
else if (status === 'corrupt')
|
||||
result.errors.push(
|
||||
`${target.label}: ${path.basename(target.file)} is corrupt — left untouched`,
|
||||
);
|
||||
else result.skipped.push(`${target.label} MCP (not configured)`);
|
||||
} catch (err: any) {
|
||||
result.errors.push(`${target.label}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await uninstallCodex(result, dryRun, targets.codex.configFile, targets.codex.tomlSection);
|
||||
|
||||
// ─── Hooks ───────────────────────────────────────────────────────
|
||||
for (const hook of targets.hooks) {
|
||||
try {
|
||||
const { status, count } = await removeHookEntries(
|
||||
hook.settingsFile,
|
||||
hook.events,
|
||||
hook.needle,
|
||||
dryRun,
|
||||
);
|
||||
if (status === 'removed')
|
||||
result.removed.push(`${hook.label} hooks (${count}) — ${hook.settingsFile}`);
|
||||
else if (status === 'corrupt')
|
||||
result.errors.push(
|
||||
`${hook.label} hooks: ${path.basename(hook.settingsFile)} is corrupt — left untouched`,
|
||||
);
|
||||
// Don't delete the hook script while a registered entry may still point
|
||||
// at it (corrupt = we couldn't parse/remove the entry) — that would
|
||||
// leave the editor invoking a missing script on every matched tool call.
|
||||
if (status !== 'corrupt' && (await removeDir(hook.scriptDir, dryRun)))
|
||||
result.removed.push(`${hook.label} hook scripts — ${hook.scriptDir}`);
|
||||
} catch (err: any) {
|
||||
result.errors.push(`${hook.label} hooks: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Skills ──────────────────────────────────────────────────────
|
||||
// Skill directories are identified by the bundled gitnexus skill names; the
|
||||
// exact paths are listed below so the user can see what will be removed.
|
||||
const skillNames = await listGitnexusSkillNames();
|
||||
for (const target of targets.skills) {
|
||||
try {
|
||||
const removedDirs = await removeSkillsFrom(target.dir, skillNames, dryRun);
|
||||
for (const dir of removedDirs) result.removed.push(`${target.label} skill — ${dir}`);
|
||||
} catch (err: any) {
|
||||
result.errors.push(`${target.label} skills: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Report ──────────────────────────────────────────────────────
|
||||
const verb = dryRun ? 'Would remove' : 'Removed';
|
||||
if (result.removed.length > 0) {
|
||||
console.log(` ${verb}:`);
|
||||
for (const name of result.removed) console.log(` - ${name}`);
|
||||
} else {
|
||||
console.log(' Nothing to remove — GitNexus is not configured in any detected editor.');
|
||||
}
|
||||
|
||||
if (result.skipped.length > 0) {
|
||||
console.log('');
|
||||
console.log(' Skipped:');
|
||||
for (const name of result.skipped) console.log(` - ${name}`);
|
||||
}
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.log('');
|
||||
console.log(' Errors:');
|
||||
for (const err of result.errors) console.log(` ! ${err}`);
|
||||
// Signal partial failure to callers/CI without aborting the remaining
|
||||
// cleanup (which has already run by this point).
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(' Note: skill directories are matched by bundled gitnexus skill name. If you');
|
||||
console.log(' customized files inside an installed skill dir, back them up before --force.');
|
||||
|
||||
console.log('');
|
||||
console.log(' Not removed automatically:');
|
||||
console.log(' - Per-repo indexes — run: gitnexus clean --all');
|
||||
console.log(' - The global npm package — run: npm uninstall -g gitnexus');
|
||||
|
||||
if (dryRun && result.removed.length > 0) {
|
||||
console.log('');
|
||||
console.log(' Re-run with --force to apply the changes above.');
|
||||
}
|
||||
console.log('');
|
||||
};
|
||||
@@ -58,21 +58,14 @@ function parsePositiveIntegerOption(
|
||||
|
||||
function isLocalProvider(
|
||||
provider: LLMProvider | undefined,
|
||||
): provider is 'cursor' | 'claude' | 'codex' | 'opencode' {
|
||||
return (
|
||||
provider === 'cursor' ||
|
||||
provider === 'claude' ||
|
||||
provider === 'codex' ||
|
||||
provider === 'opencode'
|
||||
);
|
||||
): provider is 'cursor' | 'claude' | 'codex' {
|
||||
return provider === 'cursor' || provider === 'claude' || provider === 'codex';
|
||||
}
|
||||
|
||||
function localModelConfigKey(provider: 'cursor' | 'claude' | 'codex' | 'opencode') {
|
||||
function localModelConfigKey(provider: 'cursor' | 'claude' | 'codex') {
|
||||
if (provider === 'cursor') return 'cursorModel';
|
||||
if (provider === 'claude') return 'claudeModel';
|
||||
if (provider === 'codex') return 'codexModel';
|
||||
if (provider === 'opencode') return 'opencodeModel';
|
||||
throw new Error(`Unsupported local provider: ${provider satisfies never}`);
|
||||
return 'codexModel';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,7 +248,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
||||
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
||||
console.log(' Error: No LLM API key found.');
|
||||
console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
|
||||
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
|
||||
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -263,17 +256,16 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
||||
} else {
|
||||
console.log(" No LLM configured. Let's set it up.\n");
|
||||
console.log(
|
||||
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
|
||||
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, or Codex CLI.\n',
|
||||
);
|
||||
|
||||
// Check if local agent CLIs are available.
|
||||
const hasCursor = detectCursorCLI();
|
||||
const hasClaude = detectLocalCLI('claude');
|
||||
const hasCodex = detectLocalCLI('codex');
|
||||
const hasOpenCode = detectLocalCLI('opencode');
|
||||
const localChoices: Array<{
|
||||
choice: string;
|
||||
provider: 'cursor' | 'claude' | 'codex' | 'opencode';
|
||||
provider: 'cursor' | 'claude' | 'codex';
|
||||
}> = [];
|
||||
|
||||
// Provider selection
|
||||
@@ -306,14 +298,6 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
||||
});
|
||||
console.log(` [${choice}] Codex CLI (local, uses your Codex login)`);
|
||||
}
|
||||
if (hasOpenCode) {
|
||||
const choice = String(nextChoice++);
|
||||
localChoices.push({
|
||||
choice,
|
||||
provider: 'opencode',
|
||||
});
|
||||
console.log(` [${choice}] OpenCode CLI (local, uses your OpenCode login/config)`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
const maxChoice = String(nextChoice - 1);
|
||||
|
||||
@@ -55,7 +55,6 @@ const METHOD_ANNOTATION_TO_HTTP: Record<string, string> = {
|
||||
interface SpringRouteBinding {
|
||||
method: string;
|
||||
path: string;
|
||||
ownerPrefix?: string;
|
||||
}
|
||||
|
||||
interface SpringMethodInfo {
|
||||
@@ -396,25 +395,6 @@ function joinPath(prefix: string, methodPath: string): string {
|
||||
return `/${cleanPrefix}/${cleanSub}`;
|
||||
}
|
||||
|
||||
function joinInheritedSpringPath(
|
||||
controllerPrefix: string,
|
||||
inheritedPath: string,
|
||||
inheritedOwnerPrefix = '',
|
||||
): string {
|
||||
const joined = joinPath(controllerPrefix, inheritedPath);
|
||||
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const cleanInherited = inheritedPath.replace(/^\/+/, '');
|
||||
if (!cleanPrefix) return joined;
|
||||
if (
|
||||
cleanPrefix === cleanOwnerPrefix &&
|
||||
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))
|
||||
) {
|
||||
return `/${cleanInherited}`;
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
function getNodeName(node: Parser.SyntaxNode): string | null {
|
||||
return node.childForFieldName('name')?.text ?? null;
|
||||
}
|
||||
@@ -654,7 +634,6 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
|
||||
const routes = method.routes.map((route) => ({
|
||||
method: route.method,
|
||||
path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
|
||||
ownerPrefix: type.classPrefix,
|
||||
}));
|
||||
if (routes.length > 0) methodMap.set(method.name, routes);
|
||||
}
|
||||
@@ -672,7 +651,7 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
|
||||
const routes = routeMap.get(method.name) ?? [];
|
||||
return routes.map((route) => ({
|
||||
method: route.method,
|
||||
path: joinInheritedSpringPath(type.classPrefix, route.path, route.ownerPrefix),
|
||||
path: joinPath(type.classPrefix, route.path),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { glob } from 'glob';
|
||||
import Parser from 'tree-sitter';
|
||||
import C from 'tree-sitter-c';
|
||||
import Cpp from 'tree-sitter-cpp';
|
||||
|
||||
// `tree-sitter-c` is vendored prebuild-only (#2116) and may be absent on a
|
||||
// toolchain-less / `--ignore-scripts` install. Load it via a guarded `_require`
|
||||
// rather than a top-level `import C from 'tree-sitter-c'`, which would throw
|
||||
// ERR_MODULE_NOT_FOUND at module-load and crash analyze (#2091/#2093). When the
|
||||
// binding is absent, `getLanguageForFile` returns null for `.c`/`.h` so C
|
||||
// include-extraction is skipped (C++ is unaffected — its binding always ships).
|
||||
const _require = createRequire(import.meta.url);
|
||||
let C: unknown = null;
|
||||
try {
|
||||
C = _require('tree-sitter-c');
|
||||
} catch {
|
||||
/* C grammar unavailable — C include extraction degrades to a no-op. */
|
||||
}
|
||||
import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
|
||||
import type { ExtractedContract, RepoHandle } from '../types.js';
|
||||
import { readSafe } from './fs-utils.js';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import Parser from 'tree-sitter';
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
/**
|
||||
* Minimal structural shape consumers need when reading Trees back
|
||||
* through a phase-dependency boundary. Declared here so phases that
|
||||
* receive ASTCache via `getPhaseOutput<...>` don't hand-roll their
|
||||
* own inline structural types that silently drift when ASTCache's
|
||||
* contract changes.
|
||||
*
|
||||
* Typed as `unknown` at the Tree boundary because consumers on the
|
||||
* other side of the phase-output map don't share tree-sitter's type
|
||||
* graph (e.g. COBOL's standalone processor).
|
||||
*/
|
||||
export interface ASTCacheReader {
|
||||
get(filePath: string): unknown;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
// Define the interface for the Cache
|
||||
export interface ASTCache extends ASTCacheReader {
|
||||
get: (filePath: string) => Parser.Tree | undefined;
|
||||
set: (filePath: string, tree: Parser.Tree) => void;
|
||||
clear: () => void;
|
||||
stats: () => { size: number; maxSize: number };
|
||||
}
|
||||
|
||||
export const createASTCache = (maxSize: number = 50): ASTCache => {
|
||||
const effectiveMax = Math.max(maxSize, 1);
|
||||
// Initialize the cache with a 'dispose' handler
|
||||
// This is the magic: When an item is evicted (dropped), this runs automatically.
|
||||
const cache = new LRUCache<string, Parser.Tree>({
|
||||
max: effectiveMax,
|
||||
dispose: (tree) => {
|
||||
try {
|
||||
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter
|
||||
// trees are GC-managed and .delete is absent (no-op here).
|
||||
//
|
||||
// Single-owner invariant (load-bearing under WASM): a given
|
||||
// Parser.Tree reference must live in AT MOST ONE ASTCache
|
||||
// that disposes. The parse-phase chunk-local cache clears
|
||||
// between chunks; the cross-phase `scopeTreeCache` (also an
|
||||
// ASTCache today) holds the same Tree by reference. Under
|
||||
// native tree-sitter this is benign (dispose is a no-op).
|
||||
// If/when GitNexus adopts web-tree-sitter for sequential
|
||||
// parsing, the cross-phase cache must either (a) skip
|
||||
// writing Trees that are already owned by a disposing cache,
|
||||
// or (b) use tree.copy() per entry. Failing to pick one
|
||||
// will hand freed memory to scope-resolution.
|
||||
(tree as unknown as { delete?: () => void }).delete?.();
|
||||
} catch (e) {
|
||||
logger.warn({ e }, 'Failed to delete tree from WASM memory');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
get: (filePath: string) => {
|
||||
const tree = cache.get(filePath);
|
||||
return tree; // Returns undefined if not found
|
||||
},
|
||||
|
||||
set: (filePath: string, tree: Parser.Tree) => {
|
||||
cache.set(filePath, tree);
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
cache.clear();
|
||||
},
|
||||
|
||||
stats: () => ({
|
||||
size: cache.size,
|
||||
maxSize: effectiveMax,
|
||||
}),
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
* call-processor so that the classification logic lives in one place.
|
||||
*
|
||||
* Heritage (mixins: include/extend/prepend) was previously routed here
|
||||
* but is now emitted by the scope-resolution pipeline. The router still
|
||||
* returns 'skip' for these calls so they don't become spurious call edges.
|
||||
* but is now handled by heritageExtractor.extractFromCall before the
|
||||
* call router runs. The router still returns 'skip' for these calls.
|
||||
*
|
||||
* NOTE: This file is intentionally duplicated in gitnexus-web/ because the
|
||||
* two packages have separate build targets (Node native vs WASM/browser).
|
||||
@@ -24,10 +24,9 @@ export type CallRoutingResult = RubyCallRouting | null;
|
||||
|
||||
/**
|
||||
* Per-language call router.
|
||||
* IMPORTANT: Call-routed imports are NOT sanitized by the standard import-path
|
||||
* cleaner, so any router that returns an importPath MUST validate it
|
||||
* independently (length cap, control-char rejection). See routeRubyCall for the
|
||||
* reference implementation.
|
||||
* IMPORTANT: Call-routed imports bypass preprocessImportPath(), so any router that
|
||||
* returns an importPath MUST validate it independently (length cap, control-char
|
||||
* rejection). See routeRubyCall for the reference implementation.
|
||||
*/
|
||||
export type CallRouter = (calledName: string, callNode: SyntaxNode) => CallRoutingResult;
|
||||
|
||||
@@ -83,10 +82,10 @@ export function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCal
|
||||
return { kind: 'import', importPath, isRelative };
|
||||
}
|
||||
|
||||
// ── include / extend / prepend — heritage (emitted by scope-resolution) ─
|
||||
// Call-based heritage (Ruby mixins) is emitted by the scope-resolution
|
||||
// pipeline. Return SKIP_RESULT so these calls don't fall through to normal
|
||||
// call processing and become spurious call edges.
|
||||
// ── include / extend / prepend — heritage (now handled by heritageExtractor) ─
|
||||
// Call-based heritage is intercepted by heritageExtractor.extractFromCall
|
||||
// before the call router runs. Return SKIP_RESULT so these calls don't
|
||||
// fall through to normal call processing.
|
||||
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
|
||||
return SKIP_RESULT;
|
||||
}
|
||||
|
||||
@@ -78,3 +78,100 @@ export interface CallExtractionConfig {
|
||||
*/
|
||||
typeAsReceiverHeuristic?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call-resolution DAG types
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// The call-resolution pipeline is a typed DAG:
|
||||
//
|
||||
// extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
|
||||
//
|
||||
// Provider hooks plug in at infer-receiver and select-dispatch; shared stages
|
||||
// stay language-agnostic. Stages 1-2 run in the parse worker; stages 3-6 run
|
||||
// on the main thread. DAG-internal types below are main-thread-only and never
|
||||
// serialize to the graph.
|
||||
|
||||
/**
|
||||
* DAG stage 3 output: call record with receiver type and source discriminant.
|
||||
*
|
||||
* `receiverTypeName` is resolved via TypeEnv → constructor-map → class-as-receiver →
|
||||
* mixed-chain, or synthesized by `inferImplicitReceiver`. `receiverSource` tags
|
||||
* which path won and drives MRO strategy selection in stage 4.
|
||||
*
|
||||
* Invariants:
|
||||
* - `receiverSource` MUST match how `receiverTypeName` was resolved; every
|
||||
* discriminant must have a live reader and writer.
|
||||
* - `hint` is opaque to shared stages; only the same provider's `selectDispatch` reads it.
|
||||
*
|
||||
* @see language-provider.ts § inferImplicitReceiver, selectDispatch
|
||||
*/
|
||||
export interface ReceiverEnriched {
|
||||
readonly calledName: string;
|
||||
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
||||
readonly receiverName: string | undefined;
|
||||
readonly receiverTypeName: string | undefined;
|
||||
readonly receiverSource:
|
||||
| 'none'
|
||||
| 'typed-binding'
|
||||
| 'constructor-map'
|
||||
| 'class-as-receiver'
|
||||
| 'mixed-chain'
|
||||
| 'implicit-self';
|
||||
/** Free-form hint from the provider hook; opaque to shared stages. */
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider hook output for `LanguageProvider.inferImplicitReceiver` (DAG stage 3).
|
||||
*
|
||||
* Overlay applied to `ReceiverEnriched` when an implicit receiver is synthesized.
|
||||
* Ruby example: bare `serialize` inside `Account#call_serialize` →
|
||||
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
|
||||
* receiverSource: 'implicit-self', hint: 'instance' }`
|
||||
*
|
||||
* Invariants:
|
||||
* - `receiverSource` is always `'implicit-self'` — the only variant this type produces.
|
||||
* - `callForm` is always `'member'` — the rewrite converts bare-call to method invocation.
|
||||
* - `hint` is opaque to shared stages; consumed by the same language's `selectDispatch`.
|
||||
*/
|
||||
export interface ImplicitReceiverOverride {
|
||||
readonly callForm: 'free' | 'member' | 'constructor';
|
||||
readonly receiverName: string;
|
||||
readonly receiverTypeName: string;
|
||||
readonly receiverSource: Extract<ReceiverEnriched['receiverSource'], 'implicit-self'>;
|
||||
/** Free-form language tag (e.g. Ruby sets 'singleton' for `def self.foo`
|
||||
* method bodies). Consumed by the same language's `selectDispatch` hook. */
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DAG stage 4 output: dispatch strategy for resolving the target method.
|
||||
*
|
||||
* Encodes which resolver branch to try first and an optional fallback.
|
||||
* Stage 5 delegates to `resolveMemberCall`, `resolveFreeCall`, or
|
||||
* `resolveStaticCall` based on `primary`.
|
||||
*
|
||||
* - `primary`: `'owner-scoped'` = MRO walk, `'free'` = arity-tiered global lookup,
|
||||
* `'constructor'` = type instantiation.
|
||||
* - `fallback`: Only `'free-arity-narrowed'` exists; used by Ruby implicit-self
|
||||
* to degrade to arity-tiered free lookup when the MRO walk misses.
|
||||
* - `ancestryView`: Ruby `'ruby-mixin'` only. `'singleton'` walks extend providers
|
||||
* only; a miss NEVER falls through to file-scoped lookup (enforced in
|
||||
* resolveCallTarget). `'instance'` is the default.
|
||||
*
|
||||
* Common patterns:
|
||||
* - `{primary: 'constructor'}` — constructor call
|
||||
* - `{primary: 'owner-scoped'}` — member call with known type
|
||||
* - `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}` — Ruby implicit-self
|
||||
* - `{primary: 'owner-scoped', ancestryView: 'singleton'}` — Ruby class-method call
|
||||
*
|
||||
* @see language-provider.ts § selectDispatch
|
||||
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
|
||||
*/
|
||||
export interface DispatchDecision {
|
||||
readonly primary: 'owner-scoped' | 'free' | 'constructor';
|
||||
readonly fallback?: 'free-arity-narrowed';
|
||||
readonly ancestryView?: 'instance' | 'singleton';
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
||||
@@ -45,33 +45,10 @@ export const cClassConfig: ClassExtractionConfig = {
|
||||
export const cppClassConfig: ClassExtractionConfig = {
|
||||
language: SupportedLanguages.CPlusPlus,
|
||||
typeDeclarationNodes: ['class_specifier', 'struct_specifier', 'enum_specifier'],
|
||||
// #1995: `union_specifier` is included so a type nested in a NAMED union
|
||||
// (`union U1 { struct Inner {...} }`) qualifies as `U1.Inner`. Anonymous unions
|
||||
// have no `name` child → extractScopeSegmentsFromNode returns [] → they correctly
|
||||
// contribute nothing (members inject into the enclosing scope). C uses the
|
||||
// separate cClassConfig (no qualifiedNodeId), so it is intentionally untouched.
|
||||
ancestorScopeNodeTypes: [
|
||||
'namespace_definition',
|
||||
'class_specifier',
|
||||
'struct_specifier',
|
||||
'union_specifier',
|
||||
],
|
||||
ancestorScopeNodeTypes: ['namespace_definition', 'class_specifier', 'struct_specifier'],
|
||||
// #1978: key nested-type nodes by their fully-qualified path (Outer.Inner) so
|
||||
// same-tail nested types in one TU stay distinct instead of silently merging.
|
||||
qualifiedNodeId: true,
|
||||
// #1995: anonymous namespaces have no `name` child, so the generic scope walker
|
||||
// drops them (empty segment) and two `namespace { struct Inner {} }` blocks in one
|
||||
// TU collapse onto a single `Inner` node. Give each anonymous namespace_definition
|
||||
// a deterministic per-block discriminator (its start byte — stable across the
|
||||
// sequential and worker full-file parses) so the nested types stay distinct.
|
||||
// Returning `undefined` for every other scope — named namespaces (incl. `inline
|
||||
// namespace`), classes, structs, named unions — falls through to the default
|
||||
// name-based extraction, leaving them unchanged. Anonymous UNIONS are not matched
|
||||
// here (members inject into the enclosing scope), so they keep yielding [].
|
||||
extractScopeSegments: (node) =>
|
||||
node.type === 'namespace_definition' && !node.childForFieldName?.('name')
|
||||
? [`@anon${node.startIndex}`]
|
||||
: undefined,
|
||||
extractName: (node) => {
|
||||
const nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) return undefined;
|
||||
|
||||
@@ -164,14 +164,6 @@ export function createClassExtractor(config: ClassExtractionConfig): ClassExtrac
|
||||
return extract(node, { name: simpleName })?.qualifiedName ?? null;
|
||||
},
|
||||
|
||||
// #1991: qualify a non-typeDeclaration scope node (e.g. a Ruby `module` → Trait)
|
||||
// by the same ancestor-scope walk the node-id path uses, so two same-tail nested
|
||||
// mixin modules stay distinct. extract()/extractQualifiedName cannot be reused —
|
||||
// they bail on non-typeDeclarations (a module is not in typeDeclarationNodes).
|
||||
qualifyScopeName(node: SyntaxNode, simpleName: string): string {
|
||||
return buildQualifiedName(node, simpleName);
|
||||
},
|
||||
|
||||
shouldSkipClassCapture(context): boolean {
|
||||
return config.shouldSkipClassCapture?.(context) ?? false;
|
||||
},
|
||||
|
||||
@@ -44,14 +44,6 @@ export interface ClassExtractor {
|
||||
},
|
||||
): ExtractedClassSymbol | null;
|
||||
extractQualifiedName(node: SyntaxNode, simpleName: string): string | null;
|
||||
/**
|
||||
* #1991: qualify a scope-defining node that maps to a class-like registry label
|
||||
* (e.g. a Ruby `module` → Trait) but is NOT a typeDeclaration, so it cannot go
|
||||
* through extract()/extractQualifiedName (which bail on non-typeDeclarations).
|
||||
* Walks the same ancestor scopes as the node-id path. Optional — only providers
|
||||
* that materialize such nodes implement it.
|
||||
*/
|
||||
qualifyScopeName?(node: SyntaxNode, simpleName: string): string;
|
||||
shouldSkipClassCapture?(
|
||||
context: ClassCaptureContext & { nodeLabel: ClassLikeNodeLabel },
|
||||
): boolean;
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* Determines whether a symbol (function, class, etc.) is exported/public
|
||||
* in its language. This is a pure function — safe for use in worker threads.
|
||||
*
|
||||
* Used by the language providers during worker parsing (parse-worker.ts) — the
|
||||
* sole parse path. (Sequential parsing was removed.)
|
||||
* Shared between parse-worker.ts (worker pool) and parsing-processor.ts (sequential fallback).
|
||||
*/
|
||||
|
||||
import { findSiblingChild, type SyntaxNode } from './utils/ast-helpers.js';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user