mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
e6183d70dd
19 changed files with 690 additions and 147 deletions
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
|
@ -69,6 +69,6 @@ jobs:
|
|||
- '**/test/fixtures/**'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
||||
|
|
|
|||
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
|
|
@ -53,6 +53,6 @@ jobs:
|
|||
retention-days: 5
|
||||
|
||||
- name: Upload to Security tab
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
|||
2
.github/workflows/trivy.yml
vendored
2
.github/workflows/trivy.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
|||
exit-code: '0'
|
||||
|
||||
- name: Upload to Security tab
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
sarif_file: trivy-${{ matrix.image.name }}.sarif
|
||||
category: trivy-${{ matrix.image.name }}
|
||||
|
|
|
|||
2
.github/workflows/workflow-lint.yml
vendored
2
.github/workflows/workflow-lint.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
|||
continue-on-error: true
|
||||
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
sarif_file: zizmor.sarif
|
||||
category: zizmor
|
||||
|
|
|
|||
133
gitnexus-web/package-lock.json
generated
133
gitnexus-web/package-lock.json
generated
|
|
@ -11,12 +11,12 @@
|
|||
"@langchain/anthropic": "^1.3.29",
|
||||
"@langchain/core": "^1.1.44",
|
||||
"@langchain/google-genai": "^2.1.30",
|
||||
"@langchain/langgraph": "^1.2.9",
|
||||
"@langchain/langgraph": "^1.3.2",
|
||||
"@langchain/ollama": "^1.2.6",
|
||||
"@langchain/openai": "^1.4.5",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"axios": "^1.16.0",
|
||||
"axios": "^1.16.1",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.4.3",
|
||||
"gitnexus-shared": "file:../gitnexus-shared",
|
||||
|
|
@ -48,12 +48,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
|
|
@ -1393,13 +1393,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.9.tgz",
|
||||
"integrity": "sha512-3c7BtGycHC2v9p6w/Hv8L7kEl1YnZYOQTDJtmAp3knk6JOedO7d2bYP3y0SRyhv5orUEGf/KGvx8ZsB/ideP7g==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.3.2.tgz",
|
||||
"integrity": "sha512-SL7Ktsr681R7da+1b2MVOWEbaCoFJOXEJPTGOjg4JIG4C7quWbTYC8DzxhcCxte6D/8cGp0rYDBnbKLXEpNqlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "^1.0.1",
|
||||
"@langchain/langgraph-sdk": "~1.8.9",
|
||||
"@langchain/langgraph-checkpoint": "^1.0.2",
|
||||
"@langchain/langgraph-sdk": "~1.9.4",
|
||||
"@langchain/protocol": "^0.0.15",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
|
|
@ -1407,7 +1408,7 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.40",
|
||||
"@langchain/core": "^1.1.44",
|
||||
"zod": "^3.25.32 || ^4.2.0",
|
||||
"zod-to-json-schema": "^3.x"
|
||||
},
|
||||
|
|
@ -1418,9 +1419,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.1.tgz",
|
||||
"integrity": "sha512-HM0cJLRpIsSlWBQ/xuDC67l52SqZ62Bh2Y61DX+Xorqwoh5e1KxYvfCD7GnSTbWWhjBOutvnR0vPhu4orFkZfw==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.2.tgz",
|
||||
"integrity": "sha512-F4E5Tr0nt8FGghgdscJtHw+ABzChOHeI80R7Y1pjIHdiJom6c2ieo76vL+FWiny80JmoGqhrVAEIWrw0cXKPxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^10.0.0"
|
||||
|
|
@ -1429,7 +1430,7 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.0.1"
|
||||
"@langchain/core": "^1.1.44"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": {
|
||||
|
|
@ -1446,27 +1447,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "1.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.8.10.tgz",
|
||||
"integrity": "sha512-wrB3rkRw5KAmsqezwvKP3midT4qJrV6Hj9XJMYo+cbvXC4HYpSAmyY/VriSyeTFRbLG/OP/pY2Yz+9Z54nSaXQ==",
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.9.tgz",
|
||||
"integrity": "sha512-aiWHbmqxWj5sAMwFsaB3eSGQvKpMbUKTlt9zbAC0T7IiFqDYUWi9gJUGsTdvJutAfB3P/NzC4s8ETUtUQEUlYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/protocol": "^0.0.15",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"p-queue": "^9.0.1",
|
||||
"p-retry": "^7.1.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.16",
|
||||
"@langchain/core": "^1.1.44",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19",
|
||||
"svelte": "^4.0.0 || ^5.0.0",
|
||||
"vue": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -1488,9 +1487,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz",
|
||||
"integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
|
||||
"integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.4",
|
||||
|
|
@ -1516,9 +1515,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.1.tgz",
|
||||
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
|
||||
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
|
@ -1587,6 +1586,12 @@
|
|||
"@langchain/core": "^1.1.42"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/protocol": {
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.15.tgz",
|
||||
"integrity": "sha512-MllvbpMjqHevUm+v94M422mH7XKN+wGCvJRBVROTWBotEDOATYB4Ktk2UheYP859y9o2LlhtPek5t1T9eyfAbQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz",
|
||||
|
|
@ -1684,13 +1689,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -2854,13 +2859,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
|
|
@ -3414,16 +3419,42 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
|
||||
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
|
|
@ -5444,9 +5475,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
|
||||
"integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
|
||||
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
|
|
@ -7572,13 +7603,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7591,9 +7622,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -8567,9 +8598,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@
|
|||
"@langchain/anthropic": "^1.3.29",
|
||||
"@langchain/core": "^1.1.44",
|
||||
"@langchain/google-genai": "^2.1.30",
|
||||
"@langchain/langgraph": "^1.2.9",
|
||||
"@langchain/langgraph": "^1.3.2",
|
||||
"@langchain/ollama": "^1.2.6",
|
||||
"@langchain/openai": "^1.4.5",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"axios": "^1.16.0",
|
||||
"axios": "^1.16.1",
|
||||
"d3": "^7.9.0",
|
||||
"dompurify": "^3.4.3",
|
||||
"gitnexus-shared": "file:../gitnexus-shared",
|
||||
|
|
@ -58,12 +58,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
|
|
|
|||
|
|
@ -24,22 +24,18 @@
|
|||
* V2 additionally walks class ancestors (via MRO), so base-class enclosing
|
||||
* namespaces also contribute associated namespaces.
|
||||
*
|
||||
* **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
|
||||
* function reference like `utils::worker` contributes `utils` to the associated
|
||||
* set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
|
||||
* to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
|
||||
* entities for function-type arguments come from the **parameter types and return
|
||||
* type** of each function in the overload set — NOT the function's enclosing
|
||||
* namespace. For `void worker()`, the standard-compliant associated set is empty.
|
||||
* GitNexus instead contributes the enclosing namespace of any Function/Method
|
||||
* def whose simple name matches, because it enables the dominant real-world ADL
|
||||
* pattern at reasonable precision cost.
|
||||
* Function-reference arguments follow ISO C++ `[basic.lookup.argdep]`:
|
||||
* associated entities come from the parameter types and return type of each
|
||||
* referenced function in the overload set, not from the function's enclosing
|
||||
* namespace. For `void worker()`, the associated set is empty. For
|
||||
* `void worker(api::Token)` or `api::Token make_token()`, `api` is associated
|
||||
* through `Token`.
|
||||
*
|
||||
* For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
|
||||
* workspace lookup (only contributed when a Function/Method named `worker` exists
|
||||
* in `utils`). For unqualified refs the workspace is searched for any Function
|
||||
* def with that simple name. Locally-declared function-pointer variables
|
||||
* (e.g. `void (*g)()`) and function parameters are excluded from this path.
|
||||
* For qualified refs (e.g. `utils::worker`) the workspace lookup is restricted
|
||||
* to functions/methods named `worker` in `utils`; for unqualified refs the
|
||||
* workspace is searched for matching functions/methods by simple name. Locally
|
||||
* declared function-pointer variables and function parameters are excluded
|
||||
* from this path.
|
||||
*
|
||||
* ADL candidates are merged with ordinary unqualified-lookup candidates
|
||||
* in the free-call fallback before overload narrowing.
|
||||
|
|
@ -70,6 +66,7 @@
|
|||
|
||||
import type { ParsedFile, ScopeId, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { normalizeCppParamType } from './arity-metadata.js';
|
||||
import { isCppInlineNamespaceScope } from './inline-namespaces.js';
|
||||
|
||||
/**
|
||||
|
|
@ -97,11 +94,8 @@ export interface CppAdlArgInfo {
|
|||
/** When set, the arg is a potential free-function reference (not a locally-
|
||||
* declared function-pointer variable or function parameter). Contains the
|
||||
* identifier text as written in source (e.g. `"utils::worker"` or
|
||||
* `"worker"`). GitNexus approximation: the function's enclosing namespace
|
||||
* is contributed to the ADL associated set. For qualified refs a workspace
|
||||
* lookup confirms a Function/Method with that simple name exists in the
|
||||
* namespace before contributing; for unqualified refs every namespace
|
||||
* containing a matching Function/Method def is contributed. */
|
||||
* `"worker"`). Resolution contributes associated namespaces from each
|
||||
* referenced Function/Method def's parameter and return types. */
|
||||
readonly functionRefText?: string;
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +201,12 @@ export function pickCppAdlCandidates(
|
|||
for (const arg of args) {
|
||||
collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
|
||||
if (arg.functionRefText !== undefined) {
|
||||
collectFunctionRefNamespaces(arg.functionRefText, parsedFiles, associatedNamespaces);
|
||||
collectFunctionTypeAssociatedNamespaces(
|
||||
arg.functionRefText,
|
||||
scopes,
|
||||
parsedFiles,
|
||||
associatedNamespaces,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (associatedNamespaces.size === 0) return undefined;
|
||||
|
|
@ -472,23 +471,12 @@ function findCppClassDefBySimpleName(
|
|||
}
|
||||
|
||||
/**
|
||||
* Contribute associated namespaces for a function-reference argument.
|
||||
*
|
||||
* - **Qualified refs** (`utils::worker`, `outer::inner::fn`): the namespace
|
||||
* is extracted from the qualifier text (converting `::` to `.` for dot-joined
|
||||
* QName matching). A workspace lookup then **verifies** that a Function or
|
||||
* Method def named `worker` (the simple name after the last `::`) actually
|
||||
* exists in the extracted namespace. This prevents false positives from
|
||||
* namespace-qualified variables, enum values, and static data members, which
|
||||
* also produce `qualified_identifier` AST nodes in tree-sitter-cpp (the
|
||||
* AST node type alone does not distinguish functions from non-function names).
|
||||
* - **Unqualified refs** (`worker`): the workspace is searched for any
|
||||
* Function/Method def whose simple name matches. Every distinct enclosing
|
||||
* namespace found is added — overloads across the same namespace produce
|
||||
* a single entry; GitNexus does not select a specific overload at this stage.
|
||||
* Contribute associated namespaces for a function-reference argument by walking
|
||||
* the referenced overload set's parameter and return types.
|
||||
*/
|
||||
function collectFunctionRefNamespaces(
|
||||
function collectFunctionTypeAssociatedNamespaces(
|
||||
refText: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
out: Set<string>,
|
||||
): void {
|
||||
|
|
@ -511,30 +499,130 @@ function collectFunctionRefNamespaces(
|
|||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method') continue;
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple === simpleName) {
|
||||
out.add(nsText);
|
||||
return; // Namespace confirmed; no need to scan further files.
|
||||
}
|
||||
if (simple === simpleName) collectAssociatedNamespacesForFunctionDef(def, scopes, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unqualified: search all namespace scopes for a Function def with this
|
||||
// simple name and contribute its enclosing namespace.
|
||||
// Unqualified function references are approximated workspace-wide, matching
|
||||
// the previous V1 lookup scope. The stricter part of this PR is what each
|
||||
// overload contributes: only namespaces from parameter/return types, never
|
||||
// the function's own enclosing namespace.
|
||||
for (const parsed of parsedFiles) {
|
||||
const scopesById = new Map<ScopeId, (typeof parsed.scopes)[number]>();
|
||||
for (const sc of parsed.scopes) scopesById.set(sc.id, sc);
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Namespace') continue;
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method') continue;
|
||||
const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
|
||||
if (simple !== refText) continue;
|
||||
const nsQName = computeNamespaceQName(scope, scopesById);
|
||||
if (nsQName !== '') out.add(nsQName);
|
||||
collectAssociatedNamespacesForFunctionDef(def, scopes, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectAssociatedNamespacesForFunctionDef(
|
||||
def: SymbolDefinition,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
out: Set<string>,
|
||||
): void {
|
||||
const parameterTypes = def.parameterTypeClasses?.map((typeClass) => typeClass.base);
|
||||
for (const paramType of parameterTypes ?? def.parameterTypes ?? []) {
|
||||
collectAssociatedNamespacesForFunctionTypeText(paramType, scopes, out);
|
||||
}
|
||||
if (def.returnType !== undefined) {
|
||||
collectAssociatedNamespacesForFunctionTypeText(def.returnType, scopes, out);
|
||||
}
|
||||
}
|
||||
|
||||
function collectAssociatedNamespacesForFunctionTypeText(
|
||||
typeText: string,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
out: Set<string>,
|
||||
): void {
|
||||
for (const token of extractCppTypeNameTokens(typeText)) {
|
||||
if (isIgnoredCppAdlNamespace(token.namespaceName)) continue;
|
||||
addAssociatedNamespaceForClassName(token.simpleName, scopes, out);
|
||||
if (token.namespaceName !== '') out.add(token.namespaceName);
|
||||
}
|
||||
}
|
||||
|
||||
function extractCppTypeNameTokens(typeText: string): readonly {
|
||||
readonly simpleName: string;
|
||||
readonly namespaceName: string;
|
||||
}[] {
|
||||
const cleaned = normalizeCppParamType(typeText);
|
||||
if (cleaned === '' || isPrimitiveCppAdlType(cleaned)) return [];
|
||||
const out: { simpleName: string; namespaceName: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
const tokenSource = typeText.includes('<') ? `${cleaned} ${typeText}` : cleaned;
|
||||
for (const rawToken of tokenSource.match(/[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*/g) ?? []) {
|
||||
if (isPrimitiveCppAdlType(rawToken)) continue;
|
||||
const segments = rawToken.split('::').filter((part) => part.length > 0);
|
||||
const simpleName = segments.at(-1) ?? '';
|
||||
if (simpleName === '' || isPrimitiveCppAdlType(simpleName)) continue;
|
||||
const namespaceName = segments.length > 1 ? segments.slice(0, -1).join('.') : '';
|
||||
const key = `${namespaceName}\0${simpleName}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({
|
||||
simpleName,
|
||||
namespaceName,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const CPP_ADL_PRIMITIVE_OR_KEYWORD_TYPES = new Set<string>([
|
||||
'alignas',
|
||||
'alignof',
|
||||
'auto',
|
||||
'bool',
|
||||
'char',
|
||||
'char8_t',
|
||||
'char16_t',
|
||||
'char32_t',
|
||||
'class',
|
||||
'const',
|
||||
'consteval',
|
||||
'constexpr',
|
||||
'constinit',
|
||||
'decltype',
|
||||
'double',
|
||||
'enum',
|
||||
'explicit',
|
||||
'extern',
|
||||
'float',
|
||||
'inline',
|
||||
'int',
|
||||
'long',
|
||||
'mutable',
|
||||
'noexcept',
|
||||
'null',
|
||||
'register',
|
||||
'short',
|
||||
'signed',
|
||||
'static',
|
||||
'string',
|
||||
'struct',
|
||||
'template',
|
||||
'thread_local',
|
||||
'typename',
|
||||
'union',
|
||||
'unknown',
|
||||
'unsigned',
|
||||
'void',
|
||||
'volatile',
|
||||
'wchar_t',
|
||||
'...',
|
||||
]);
|
||||
|
||||
function isPrimitiveCppAdlType(typeText: string): boolean {
|
||||
return CPP_ADL_PRIMITIVE_OR_KEYWORD_TYPES.has(typeText);
|
||||
}
|
||||
|
||||
function isIgnoredCppAdlNamespace(namespaceName: string): boolean {
|
||||
return namespaceName === 'std' || namespaceName.startsWith('std.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,14 @@ export function emitCppScopeCaptures(
|
|||
JSON.stringify(arity.parameterTypeClasses),
|
||||
);
|
||||
}
|
||||
const returnType = extractCppDeclarationReturnType(fnNode);
|
||||
if (returnType !== undefined) {
|
||||
grouped['@declaration.return-type'] = syntheticCapture(
|
||||
'@declaration.return-type',
|
||||
fnNode,
|
||||
returnType,
|
||||
);
|
||||
}
|
||||
if (hasExplicitSpecifier(fnNode)) {
|
||||
grouped['@declaration.is-explicit'] = syntheticCapture(
|
||||
'@declaration.is-explicit',
|
||||
|
|
@ -417,6 +425,30 @@ export function emitCppScopeCaptures(
|
|||
return out;
|
||||
}
|
||||
|
||||
function extractCppDeclarationReturnType(fnNode: SyntaxNode): string | undefined {
|
||||
const typeNode = fnNode.childForFieldName('type');
|
||||
if (typeNode === null) return undefined;
|
||||
const funcDeclarator = findFunctionDeclarator(fnNode);
|
||||
if (funcDeclarator !== null && isCppUnsupportedReturnTypeDeclarator(funcDeclarator)) {
|
||||
return undefined;
|
||||
}
|
||||
const typeText = typeNode.text.trim();
|
||||
if (typeText !== 'auto') return typeText.length > 0 ? typeText : undefined;
|
||||
if (funcDeclarator === null) return typeText;
|
||||
for (let i = 0; i < funcDeclarator.namedChildCount; i++) {
|
||||
const child = funcDeclarator.namedChild(i);
|
||||
if (child?.type !== 'trailing_return_type') continue;
|
||||
const typeDesc = child.firstNamedChild;
|
||||
return typeDesc?.text.trim() || typeText;
|
||||
}
|
||||
return typeText;
|
||||
}
|
||||
|
||||
function isCppUnsupportedReturnTypeDeclarator(funcDeclarator: SyntaxNode): boolean {
|
||||
const text = funcDeclarator.text;
|
||||
return /\boperator\b/.test(text) || /(^|[(:\s])~\s*[A-Za-z_]\w*/.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk every C++ class/struct base clause and emit `@reference.inherits`
|
||||
* captures for each base so scope resolution can resolve them into EXTENDS
|
||||
|
|
|
|||
|
|
@ -1087,6 +1087,7 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
|
|||
'@declaration.required-parameter-count',
|
||||
'@declaration.parameter-types',
|
||||
'@declaration.parameter-type-classes',
|
||||
'@declaration.return-type',
|
||||
'@declaration.template-constraints',
|
||||
'@declaration.is-explicit',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -2907,9 +2907,11 @@ export class LocalBackend {
|
|||
limit?: number;
|
||||
offset?: number;
|
||||
summaryOnly?: boolean;
|
||||
skipPerSymbolEnrichment?: boolean;
|
||||
},
|
||||
): Promise<any> {
|
||||
const { maxDepth, relationTypes, includeTests, minConfidence } = opts;
|
||||
const skipPerSymbolEnrichment = opts.skipPerSymbolEnrichment ?? false;
|
||||
const hasExplicitLimit = typeof opts.limit === 'number' && Number.isFinite(opts.limit);
|
||||
const paginationLimit = hasExplicitLimit
|
||||
? Math.max(1, Math.min(Math.trunc(opts.limit!), 10000))
|
||||
|
|
@ -3066,13 +3068,25 @@ export class LocalBackend {
|
|||
const directCount = (grouped[1] || []).length;
|
||||
let affectedProcesses: any[] = [];
|
||||
let affectedModules: any[] = [];
|
||||
// Per-symbol process membership: maps impacted symbol id -> list of processes
|
||||
// it participates in. Populated by a second chunked Cypher pass below when
|
||||
// any process is affected at all. Surfaced as `processes: [...]` on each
|
||||
// byDepth item so consumers can tell which caller belongs to which cron/
|
||||
// webhook/route without a follow-up query.
|
||||
const perSymbolProcesses = new Map<
|
||||
string,
|
||||
Array<{ id: string; label: string; processType: string; step: number }>
|
||||
>();
|
||||
|
||||
// Chunking bounds for batched DB round-trips. Declared at function scope so
|
||||
// both the in-block enrichment passes and the post-pagination per-symbol
|
||||
// process enrichment can reference them.
|
||||
const CHUNK_SIZE = 100;
|
||||
// Max number of chunks to process to avoid unbounded DB round-trips.
|
||||
// Configurable via env IMPACT_MAX_CHUNKS, default 10 => max items = 1000
|
||||
const MAX_CHUNKS = parseInt(process.env.IMPACT_MAX_CHUNKS || '10', 10);
|
||||
|
||||
if (impacted.length > 0) {
|
||||
const CHUNK_SIZE = 100;
|
||||
// Max number of chunks to process to avoid unbounded DB round-trips.
|
||||
// Configurable via env IMPACT_MAX_CHUNKS, default 10 => max items = 1000
|
||||
const MAX_CHUNKS = parseInt(process.env.IMPACT_MAX_CHUNKS || '10', 10);
|
||||
|
||||
// ── Process enrichment: batched chunking (bounded by MAX_CHUNKS) ─
|
||||
// Uses merged Cypher query (WITH + OPTIONAL MATCH) to fetch
|
||||
// process + entry point info in 1 round-trip per chunk. Converted to
|
||||
|
|
@ -3218,6 +3232,10 @@ export class LocalBackend {
|
|||
}))
|
||||
.sort((a, b) => b.total_hits - a.total_hits);
|
||||
|
||||
// Per-symbol process membership is populated post-pagination (see below)
|
||||
// so it covers exactly the symbols returned in byDepth, not a pre-capped
|
||||
// flat slice that could miss depth-2+ symbols when depth-1 is large.
|
||||
|
||||
// ── Module enrichment: use same cap as process enrichment and parameterized queries
|
||||
const maxItems = Math.min(impacted.length, MAX_CHUNKS * CHUNK_SIZE);
|
||||
const cappedImpacted = impacted.slice(0, maxItems);
|
||||
|
|
@ -3360,7 +3378,7 @@ export class LocalBackend {
|
|||
return base;
|
||||
}
|
||||
|
||||
// Apply limit/offset pagination per depth level
|
||||
// Apply limit/offset pagination per depth level.
|
||||
const paginatedGrouped: Record<number, any[]> = {};
|
||||
let anyTruncated = false;
|
||||
for (const [depth, items] of Object.entries(grouped)) {
|
||||
|
|
@ -3372,8 +3390,82 @@ export class LocalBackend {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Per-symbol process membership enrichment (post-pagination) ───────
|
||||
// Runs after paginatedGrouped is built so we enrich only the IDs that
|
||||
// actually appear in the response. This eliminates the false-empty
|
||||
// processes:[] case where a depth-2+ symbol's flat position in `impacted`
|
||||
// exceeded MAX_CHUNKS*CHUNK_SIZE even though it is returned by byDepth.
|
||||
// Also uses DISTINCT + MIN(r.step) per (symbol, process) pair to avoid
|
||||
// duplicate entries when a symbol has multiple STEP_IN_PROCESS edges.
|
||||
// Skipped entirely when `skipPerSymbolEnrichment` is set (group cross-repo
|
||||
// fan-out, which consumes byDepth but not byDepth[].processes); the
|
||||
// attach-loop below still stamps an empty processes:[] for shape stability.
|
||||
let perSymbolEnrichmentCapped = false;
|
||||
if (affectedProcesses.length > 0 && !skipPerSymbolEnrichment) {
|
||||
// Collect unique IDs from the paginated result in one pass.
|
||||
const pageIds = new Set<string>();
|
||||
for (const items of Object.values(paginatedGrouped)) {
|
||||
for (const it of items) {
|
||||
const id = String(it.id ?? '');
|
||||
if (id) pageIds.add(id);
|
||||
}
|
||||
}
|
||||
// Bound the enrichment to the same ceiling as the aggregation pass
|
||||
// (MAX_CHUNKS * CHUNK_SIZE) so a large paginated page cannot trigger
|
||||
// unbounded DB round-trips (DoD 2.6). When capped, mark the result
|
||||
// partial so callers know some returned symbols may carry an empty
|
||||
// processes:[] that is a cap artifact, not a true absence.
|
||||
const maxPageIds = MAX_CHUNKS * CHUNK_SIZE;
|
||||
let pageIdArr = Array.from(pageIds);
|
||||
if (pageIdArr.length > maxPageIds) {
|
||||
pageIdArr = pageIdArr.slice(0, maxPageIds);
|
||||
perSymbolEnrichmentCapped = true;
|
||||
}
|
||||
for (let i = 0; i < pageIdArr.length; i += CHUNK_SIZE) {
|
||||
const chunkIds = pageIdArr.slice(i, i + CHUNK_SIZE);
|
||||
try {
|
||||
const rows = await executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
WHERE s.id IN $ids
|
||||
RETURN s.id AS sid, p.id AS pid, p.heuristicLabel AS pName,
|
||||
p.processType AS pType, MIN(r.step) AS step
|
||||
`,
|
||||
{ ids: chunkIds },
|
||||
).catch(() => []);
|
||||
for (const row of rows) {
|
||||
const sid = row.sid ?? row[0];
|
||||
if (!sid) continue;
|
||||
const procEntry = {
|
||||
id: String(row.pid ?? row[1] ?? ''),
|
||||
label: String(row.pName ?? row[2] ?? ''),
|
||||
processType: String(row.pType ?? row[3] ?? ''),
|
||||
step: Number(row.step ?? row[4] ?? -1),
|
||||
};
|
||||
const list = perSymbolProcesses.get(String(sid));
|
||||
if (list) list.push(procEntry);
|
||||
else perSymbolProcesses.set(String(sid), [procEntry]);
|
||||
}
|
||||
} catch (e) {
|
||||
logQueryError('impact:per-symbol-process-chunk', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attach processes field to each paginated item.
|
||||
for (const items of Object.values(paginatedGrouped)) {
|
||||
for (const it of items) {
|
||||
it.processes = perSymbolProcesses.get(String(it.id)) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
// Surface partial if the per-symbol enrichment was capped, even when the
|
||||
// BFS traversal itself completed — some returned symbols may carry an
|
||||
// empty processes:[] that is a cap artifact rather than a true absence.
|
||||
...(perSymbolEnrichmentCapped && { partial: true }),
|
||||
...(anyTruncated && {
|
||||
pagination: {
|
||||
...(Number.isFinite(paginationLimit) && { limit: paginationLimit }),
|
||||
|
|
@ -3467,11 +3559,20 @@ export class LocalBackend {
|
|||
];
|
||||
|
||||
try {
|
||||
// skipPerSymbolEnrichment suppresses ONLY the per-symbol STEP_IN_PROCESS
|
||||
// enrichment pass while preserving byDepth. Group-mode cross-repo fan-out
|
||||
// may fan across many repos; the per-symbol pass adds up to MAX_CHUNKS
|
||||
// extra round-trips per repo, which is unacceptable at group scale. But
|
||||
// cross-impact fan-out DOES consume byDepth (cross-impact.ts reads
|
||||
// fan.byDepth to populate group by_depth), so summaryOnly would wrongly
|
||||
// drop it. Group callers do not consume byDepth[].processes, so skipping
|
||||
// only that enrichment is the correct, targeted suppression.
|
||||
return await this._runImpactBFS(repo, sym, symType, dir, {
|
||||
maxDepth: opts.maxDepth,
|
||||
relationTypes,
|
||||
includeTests: opts.includeTests,
|
||||
minConfidence: opts.minConfidence,
|
||||
skipPerSymbolEnrichment: true,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ Output includes:
|
|||
- summary: direct callers, processes affected, modules affected
|
||||
- affected_processes: which execution flows break and at which step
|
||||
- affected_modules: which functional areas are hit (direct vs indirect)
|
||||
- byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated)
|
||||
- byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated). Each item includes a processes:[{id,label,processType,step}] field listing the execution flows that symbol participates in. Empty when the symbol has no process membership. Can ALSO be empty when partial:true is set — either the process-aggregation pass hit its cap before detecting affected processes, or per-symbol enrichment was capped on a very large page. When partial:true, do NOT treat processes:[] as proof of no participation; cross-check the top-level affected_processes list.
|
||||
|
||||
Depth groups:
|
||||
- d=1: WILL BREAK (direct callers/importers)
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-return-strict/app.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-return-strict/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "lib.h"
|
||||
|
||||
namespace caller {
|
||||
void run() {
|
||||
run_callback(utils::make_token);
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-return-strict/lib.h
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-return-strict/lib.h
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace api {
|
||||
struct Token {
|
||||
friend void run_callback(Token t) {}
|
||||
};
|
||||
}
|
||||
|
||||
namespace utils {
|
||||
api::Token make_token();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-strict/app.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-strict/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "lib.h"
|
||||
|
||||
namespace caller {
|
||||
void run() {
|
||||
run_callback(utils::worker);
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-strict/lib.h
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/cpp-adl-free-func-ref-strict/lib.h
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
namespace api {
|
||||
struct Token {
|
||||
friend void run_callback(Token t) {}
|
||||
};
|
||||
}
|
||||
|
||||
namespace utils {
|
||||
void worker(api::Token token);
|
||||
}
|
||||
|
|
@ -2963,41 +2963,66 @@ describe('C++ ADL — block-scope function declaration suppresses ADL', () => {
|
|||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ADL V2 — free-function reference args contribute their namespace.
|
||||
// ADL V2 - strict function-type associated entities.
|
||||
//
|
||||
// GitNexus approximation (not strict ISO C++ ADL): when a qualified_identifier
|
||||
// like `utils::worker` is passed as an argument, GitNexus contributes the
|
||||
// enclosing namespace (`utils`) to the associated set, provided a Function or
|
||||
// Method named `worker` is found in the `utils` namespace at resolution time.
|
||||
// Under ISO C++ [basic.lookup.argdep] the associated entities for a function-type
|
||||
// argument come from the parameter types and return type of the overload set —
|
||||
// NOT the function's enclosing namespace. For `void worker()`, the standard-
|
||||
// compliant associated set is empty. The approximation captures the dominant
|
||||
// real-world pattern (pass a utility function → find its sibling) at the cost
|
||||
// of potential false positives when an unrelated function with the same simple
|
||||
// name exists in the same namespace (bounded by the workspace-function lookup).
|
||||
// Function-reference arguments follow strict ISO C++ ADL: GitNexus walks the
|
||||
// referenced overload set's parameter and return types instead of contributing
|
||||
// the referenced function's enclosing namespace.
|
||||
// For `void worker()`, the associated set is empty; for `void worker(api::Token)`
|
||||
// or `api::Token make_token()`, `api` is associated through `Token`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C++ ADL — qualified free-function reference contributes its namespace', () => {
|
||||
describe('C++ ADL - free-function reference does not contribute its namespace', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-free-func-ref'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('with_callback(utils::worker) resolves to utils::with_callback via ADL', () => {
|
||||
it('with_callback(utils::worker) emits zero CALLS edges when worker has no class parameter or return type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback');
|
||||
// Ordinary lookup inside caller::run finds nothing (no `using`, no local
|
||||
// declaration). utils::worker is a qualified_identifier argument, so ADL
|
||||
// contributes `utils` to the associated-namespace set. utils::with_callback
|
||||
// is then discovered as the sole candidate.
|
||||
expect(cbCalls.length).toBe(1);
|
||||
expect(cbCalls[0].targetFilePath).toContain('utils.h');
|
||||
expect(cbCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL — overloaded free-function reference does not crash', () => {
|
||||
describe('C++ ADL - free-function reference contributes parameter-type associated namespace', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-free-func-ref-strict'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('run_callback(utils::worker) resolves hidden friend through worker(api::Token)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_callback');
|
||||
expect(cbCalls.length).toBe(1);
|
||||
expect(cbCalls[0].targetFilePath).toContain('lib.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL - free-function reference contributes return-type associated namespace', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'cpp-adl-free-func-ref-return-strict'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('run_callback(utils::make_token) resolves hidden friend through api::Token return type', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_callback');
|
||||
expect(cbCalls.length).toBe(1);
|
||||
expect(cbCalls[0].targetFilePath).toContain('lib.h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ ADL - overloaded free-function reference stays strict', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
@ -3007,15 +3032,10 @@ describe('C++ ADL — overloaded free-function reference does not crash', () =>
|
|||
);
|
||||
}, 60000);
|
||||
|
||||
it('with_callback(utils::worker) with overloaded utils::worker still resolves utils::with_callback via ADL', () => {
|
||||
it('with_callback(utils::worker) with overloaded utils::worker still emits zero CALLS edges', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback');
|
||||
// utils::worker has two overloads (worker() and worker(int)). V1
|
||||
// simplification: contribute the namespace if ANY overload exists in the
|
||||
// workspace, regardless of which one would be selected. The namespace
|
||||
// `utils` is still added, and utils::with_callback is discovered.
|
||||
expect(cbCalls.length).toBe(1);
|
||||
expect(cbCalls[0].targetFilePath).toContain('utils.h');
|
||||
expect(cbCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -3035,10 +3055,10 @@ describe('C++ ADL — namespace-qualified variable arg does NOT contribute names
|
|||
// data::value is a namespace-qualified integer variable. tree-sitter-cpp
|
||||
// produces a qualified_identifier AST node regardless of whether `value`
|
||||
// denotes a function, variable, enum, or static member. The GitNexus guard
|
||||
// in collectFunctionRefNamespaces verifies that a Function/Method named
|
||||
// `value` exists in the `data` namespace before contributing it. Since
|
||||
// `data::value` is an int variable, `data` is never added to the associated
|
||||
// set, so data::process is never found as an ADL candidate.
|
||||
// in collectFunctionTypeAssociatedNamespaces verifies that a Function/Method
|
||||
// named `value` exists in the `data` namespace before walking any function
|
||||
// type. Since `data::value` is an int variable, no function type is walked,
|
||||
// so data::process is never found as an ADL candidate.
|
||||
expect(processCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -363,6 +363,11 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
// Scope-resolver-only correctness wins; backporting is out of scope.
|
||||
'process(data::value) emits zero CALLS edges \u2014 data::value is a variable, not a function',
|
||||
'run_with(callback) emits zero CALLS edges when callback is a parameter, not a function reference',
|
||||
// PR #1633: strict function-type ADL no longer contributes the referenced
|
||||
// function's enclosing namespace. The legacy DAG still resolves these via
|
||||
// simple-name global fallback.
|
||||
'with_callback(utils::worker) emits zero CALLS edges when worker has no class parameter or return type',
|
||||
'with_callback(utils::worker) with overloaded utils::worker still emits zero CALLS edges',
|
||||
// PR #1599 adversarial review findings: nearest-scope ADL blocker
|
||||
// semantics and block-scope function declaration ADL suppression are
|
||||
// scope-resolver-only. The legacy DAG has no scope-aware ADL blocker
|
||||
|
|
|
|||
|
|
@ -647,6 +647,229 @@ describe('LocalBackend.callTool', () => {
|
|||
expect(result.target).toBeDefined();
|
||||
});
|
||||
|
||||
it('impact byDepth items include a processes field (default empty when no processes)', async () => {
|
||||
// Resolver returns target; BFS returns one frontier caller; no STEP_IN_PROCESS rows.
|
||||
(executeParameterized as any).mockResolvedValue([
|
||||
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
]);
|
||||
(executeQuery as any).mockResolvedValue([
|
||||
{
|
||||
id: 'func:caller',
|
||||
name: 'caller',
|
||||
type: 'Function',
|
||||
filePath: 'src/uses-main.ts',
|
||||
relType: 'CALLS',
|
||||
confidence: 0.9,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await backend.callTool('impact', { target: 'main', direction: 'upstream' });
|
||||
const d1 = result.byDepth?.[1] || result.byDepth?.['1'] || [];
|
||||
expect(d1.length).toBeGreaterThan(0);
|
||||
for (const item of d1) {
|
||||
expect(item).toHaveProperty('processes');
|
||||
expect(Array.isArray(item.processes)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('impact populates byDepth processes when STEP_IN_PROCESS rows exist', async () => {
|
||||
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
|
||||
// Symbol resolver name-lookup
|
||||
if (cypher.includes('WHERE n.name =')) {
|
||||
return Promise.resolve([
|
||||
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
]);
|
||||
}
|
||||
// Aggregation pass (must return at least one row so per-symbol pass is gated open)
|
||||
if (cypher.includes('COUNT(DISTINCT s.id)')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
pId: 'proc:cron_daily',
|
||||
name: 'Daily cron',
|
||||
heuristicLabel: 'Daily cron',
|
||||
processType: 'cron',
|
||||
entryPointId: 'func:cron_entry',
|
||||
hits: 1,
|
||||
minStep: 1,
|
||||
stepCount: 5,
|
||||
epName: 'cron_entry',
|
||||
epType: 'Function',
|
||||
epFilePath: 'src/cron.ts',
|
||||
},
|
||||
]);
|
||||
}
|
||||
// New per-symbol pass added by this change
|
||||
if (cypher.includes('RETURN s.id AS sid')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
sid: 'func:caller',
|
||||
pid: 'proc:cron_daily',
|
||||
pName: 'Daily cron',
|
||||
pType: 'cron',
|
||||
step: 2,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
(executeQuery as any).mockResolvedValue([
|
||||
{
|
||||
id: 'func:caller',
|
||||
name: 'caller',
|
||||
type: 'Function',
|
||||
filePath: 'src/uses-main.ts',
|
||||
relType: 'CALLS',
|
||||
confidence: 0.9,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await backend.callTool('impact', { target: 'main', direction: 'upstream' });
|
||||
const d1 = result.byDepth?.[1] || result.byDepth?.['1'] || [];
|
||||
const caller = d1.find((it: any) => it.id === 'func:caller');
|
||||
expect(caller).toBeDefined();
|
||||
expect(caller.processes).toHaveLength(1);
|
||||
expect(caller.processes[0]).toMatchObject({
|
||||
id: 'proc:cron_daily',
|
||||
label: 'Daily cron',
|
||||
processType: 'cron',
|
||||
step: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('impact summaryOnly:true skips the per-symbol STEP_IN_PROCESS enrichment pass', async () => {
|
||||
// Resolver returns target; BFS returns one caller; aggregation returns one process row.
|
||||
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
|
||||
if (cypher.includes('WHERE n.name =')) {
|
||||
return Promise.resolve([
|
||||
{ id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
]);
|
||||
}
|
||||
if (cypher.includes('COUNT(DISTINCT s.id)')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
pId: 'proc:daily',
|
||||
name: 'Daily cron',
|
||||
heuristicLabel: 'Daily cron',
|
||||
processType: 'cron',
|
||||
entryPointId: 'func:cron_entry',
|
||||
hits: 1,
|
||||
minStep: 1,
|
||||
stepCount: 5,
|
||||
epName: 'cron_entry',
|
||||
epType: 'Function',
|
||||
epFilePath: 'src/cron.ts',
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
(executeQuery as any).mockResolvedValue([
|
||||
{
|
||||
id: 'func:caller',
|
||||
name: 'caller',
|
||||
type: 'Function',
|
||||
filePath: 'src/a.ts',
|
||||
relType: 'CALLS',
|
||||
confidence: 0.9,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'upstream',
|
||||
summaryOnly: true,
|
||||
});
|
||||
|
||||
// summaryOnly should return base fields only, no byDepth
|
||||
expect(result.summary).toBeDefined();
|
||||
expect(result.byDepth).toBeUndefined();
|
||||
|
||||
// The per-symbol enrichment query contains 'RETURN s.id AS sid'; verify it
|
||||
// was never called (the gate should have suppressed it).
|
||||
const perSymbolCalls = (executeParameterized as any).mock.calls.filter(
|
||||
([, cypher]: [string, string]) =>
|
||||
typeof cypher === 'string' && cypher.includes('RETURN s.id AS sid'),
|
||||
);
|
||||
expect(perSymbolCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('impactByUid preserves byDepth while skipping per-symbol enrichment (group fan-out)', async () => {
|
||||
// Regression guard for the cross-repo by_depth contract: impactByUid must
|
||||
// suppress only the per-symbol STEP_IN_PROCESS pass, NOT the whole byDepth
|
||||
// field. cross-impact.ts reads fan.byDepth to populate group `by_depth`;
|
||||
// using summaryOnly here would silently empty it.
|
||||
//
|
||||
// impactByUid takes an explicit repoId and calls refreshRepos() internally.
|
||||
// Use a fresh backend whose repo path is already absolute/resolved so the
|
||||
// derived repoId stays stable across that refresh (an unresolved POSIX
|
||||
// fixture path triggers the path-collision rehash and drops the key).
|
||||
const resolvedRepoPath = path.resolve('/tmp/test-project');
|
||||
(listRegisteredRepos as any).mockResolvedValue([
|
||||
{ ...MOCK_REPO_ENTRY, path: resolvedRepoPath },
|
||||
]);
|
||||
backend = new LocalBackend();
|
||||
await backend.init();
|
||||
|
||||
(executeParameterized as any).mockImplementation((_repoId: string, cypher: string) => {
|
||||
// UID resolver
|
||||
if (cypher.includes('WHERE n.id = $uid')) {
|
||||
return Promise.resolve([
|
||||
{ id: 'func:main', name: 'main', filePath: 'src/index.ts', type: 'Function' },
|
||||
]);
|
||||
}
|
||||
// Aggregation pass (returns a process row so affectedProcesses > 0; if the
|
||||
// per-symbol pass were not skipped, this would open its gate)
|
||||
if (cypher.includes('COUNT(DISTINCT s.id)')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
pId: 'proc:daily',
|
||||
name: 'Daily cron',
|
||||
heuristicLabel: 'Daily cron',
|
||||
processType: 'cron',
|
||||
entryPointId: 'func:cron_entry',
|
||||
hits: 1,
|
||||
minStep: 1,
|
||||
stepCount: 5,
|
||||
epName: 'cron_entry',
|
||||
epType: 'Function',
|
||||
epFilePath: 'src/cron.ts',
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
(executeQuery as any).mockResolvedValue([
|
||||
{
|
||||
id: 'func:caller',
|
||||
name: 'caller',
|
||||
type: 'Function',
|
||||
filePath: 'src/uses-main.ts',
|
||||
relType: 'CALLS',
|
||||
confidence: 0.9,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await backend.impactByUid('test-project', 'uid:main', 'upstream', {
|
||||
maxDepth: 5,
|
||||
relationTypes: ['CALLS'],
|
||||
minConfidence: 0,
|
||||
includeTests: true,
|
||||
});
|
||||
|
||||
// byDepth must survive (Finding A regression guard)
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.byDepth).toBeDefined();
|
||||
const d1 = result.byDepth?.[1] || result.byDepth?.['1'] || [];
|
||||
expect(d1.find((it: any) => it.id === 'func:caller')).toBeDefined();
|
||||
|
||||
// The per-symbol enrichment query must never fire under skipPerSymbolEnrichment
|
||||
const perSymbolCalls = (executeParameterized as any).mock.calls.filter(
|
||||
([, cypher]: [string, string]) =>
|
||||
typeof cypher === 'string' && cypher.includes('RETURN s.id AS sid'),
|
||||
);
|
||||
expect(perSymbolCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('dispatches detect_changes tool', async () => {
|
||||
// detect_changes calls execFileSync which we haven't mocked at module level,
|
||||
// so it will throw a git error — that's fine, we test the error path
|
||||
|
|
|
|||
|
|
@ -97,7 +97,10 @@ describe('impact: batching and grouping', () => {
|
|||
executeParameterizedMock.mockImplementation(async (...args: any[]) => {
|
||||
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
|
||||
const params = args[2] || {};
|
||||
if (query.includes('STEP_IN_PROCESS')) {
|
||||
// Match only the aggregation chunk (which uses COUNT(DISTINCT s.id)),
|
||||
// not the per-symbol enrichment pass added by impact byDepth processes
|
||||
// (which also matches STEP_IN_PROCESS but has a different RETURN shape).
|
||||
if (query.includes('STEP_IN_PROCESS') && query.includes('COUNT(DISTINCT s.id)')) {
|
||||
// Count ids passed in as params.ids
|
||||
const ids = Array.isArray(params.ids) ? params.ids : [];
|
||||
const cnt = ids.length;
|
||||
|
|
@ -263,7 +266,10 @@ describe('impact: batching and grouping', () => {
|
|||
executeParameterizedMock.mockImplementation(async (...args: any[]) => {
|
||||
const query = typeof args[1] === 'string' ? args[1] : String(args[0] ?? '');
|
||||
const params = args[2] || {};
|
||||
if (query.includes('STEP_IN_PROCESS')) {
|
||||
// Match only the aggregation chunk (which uses COUNT(DISTINCT s.id)),
|
||||
// not the per-symbol enrichment pass added by impact byDepth processes
|
||||
// (which also matches STEP_IN_PROCESS but has a different RETURN shape).
|
||||
if (query.includes('STEP_IN_PROCESS') && query.includes('COUNT(DISTINCT s.id)')) {
|
||||
const ids = Array.isArray(params.ids) ? params.ids : [];
|
||||
chunkSizes.push(ids.length);
|
||||
return [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue