diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 93dcf9ce3..0cd526768 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -135,7 +135,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Install Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Log in to GitHub Container Registry if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }} diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index fa15ce15d..cf69e1083 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@langchain/anthropic": "^1.3.29", "@langchain/core": "^1.1.44", - "@langchain/google-genai": "^2.1.28", + "@langchain/google-genai": "^2.1.30", "@langchain/langgraph": "^1.2.9", "@langchain/ollama": "^1.2.6", "@langchain/openai": "^1.4.5", @@ -60,7 +60,7 @@ "jsdom": "^29.1.1", "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", - "vite": "^8.0.10", + "vite": "^8.0.11", "vitest": "^4.1.5", "wait-on": "^9.0.5" }, @@ -1402,32 +1402,18 @@ } }, "node_modules/@langchain/google-genai": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-2.1.28.tgz", - "integrity": "sha512-iTzNYWST8hTRqOXZdme18tq5GnCUwtrrJECE51ZCjg6Vg0mPsV44amdC+/bc+UK0+uphKWESGWs277aAyM2MlA==", + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-2.1.30.tgz", + "integrity": "sha512-0wKgy1NvV89fw5MwYiOOhh18SnUEH20z6MZrPV6Tj2hMAA3jAHVSLlIcCQ2mDRJo2r1aHLV8MDXhzkvD1tEHoQ==", "license": "MIT", "dependencies": { - "@google/generative-ai": "^0.24.0", - "uuid": "^11.1.0" + "@google/generative-ai": "^0.24.0" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@langchain/core": "^1.1.41" - } - }, - "node_modules/@langchain/google-genai/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" + "@langchain/core": "^1.1.43" } }, "node_modules/@langchain/langgraph": { @@ -1713,9 +1699,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -1738,9 +1724,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", "cpu": [ "arm64" ], @@ -1754,9 +1740,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", "cpu": [ "arm64" ], @@ -1770,9 +1756,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", "cpu": [ "x64" ], @@ -1786,9 +1772,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", "cpu": [ "x64" ], @@ -1802,9 +1788,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", "cpu": [ "arm" ], @@ -1818,9 +1804,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", "cpu": [ "arm64" ], @@ -1834,9 +1820,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", "cpu": [ "arm64" ], @@ -1850,9 +1836,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", "cpu": [ "ppc64" ], @@ -1866,9 +1852,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", "cpu": [ "s390x" ], @@ -1882,9 +1868,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", "cpu": [ "x64" ], @@ -1898,9 +1884,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", "cpu": [ "x64" ], @@ -1914,9 +1900,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", "cpu": [ "arm64" ], @@ -1930,9 +1916,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", "cpu": [ "wasm32" ], @@ -1948,9 +1934,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", "cpu": [ "arm64" ], @@ -1964,9 +1950,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", "cpu": [ "x64" ], @@ -7136,9 +7122,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -7515,9 +7501,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -7867,13 +7853,13 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" @@ -7882,27 +7868,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", "license": "MIT" }, "node_modules/roughjs": { @@ -8622,15 +8608,15 @@ } }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "bin": { @@ -8647,7 +8633,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 18eb43caf..211bb65c7 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -21,7 +21,7 @@ "gitnexus-shared": "file:../gitnexus-shared", "@langchain/anthropic": "^1.3.29", "@langchain/core": "^1.1.44", - "@langchain/google-genai": "^2.1.28", + "@langchain/google-genai": "^2.1.30", "@langchain/langgraph": "^1.2.9", "@langchain/ollama": "^1.2.6", "@langchain/openai": "^1.4.5", @@ -70,7 +70,7 @@ "jsdom": "^29.1.1", "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", - "vite": "^8.0.10", + "vite": "^8.0.11", "vitest": "^4.1.5", "wait-on": "^9.0.5" } diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 8e18fed7d..42dcb7aa7 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -199,7 +199,9 @@ async function fileExists(filePath: string): Promise { async function upsertGitNexusSection( filePath: string, content: string, -): Promise<'created' | 'updated' | 'appended'> { + projectName: string, + stats: RepoStats, +): Promise<'created' | 'updated' | 'appended' | 'preserved'> { const exists = await fileExists(filePath); if (!exists) { @@ -223,7 +225,50 @@ async function upsertGitNexusSection( ); if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { - // Replace existing section + const existingSection = existingContent.substring( + startIdx, + endIdx + GITNEXUS_END_MARKER.length, + ); + + // If the existing section contains , preserve the user's + // custom layout and only update the stats line (node/edge/flow counts). + // This lets teams trim the verbose default template to a lean format without + // having it overwritten on every `gitnexus analyze`. + // + // Note: the keep-marker check operates on `existingSection` (the substring + // between valid section markers identified by findSectionMarkerIndex), so + // a keep marker in user prose OUTSIDE the GitNexus block has no effect. + if (existingSection.includes('')) { + // Build the new stats line from the caller-provided values directly. + // We do NOT re-extract from `content` because: + // (a) first-bold extraction is fragile if the template evolves + // (b) the parenthesized-text fallback can match unrelated tuples + // like `({target: "symbolName", direction: "upstream"})` + // when noStats is set + // Passing projectName + stats explicitly makes the contract obvious. + // noStats controls template generation, not keep-section stat updates — the user opted into a stats line by keeping it. + const newStatsInner = `${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows`; + const statsLine = `Indexed as **${projectName}** (${newStatsInner})`; + + // Match either canonical phrasing at line start (`^` with `m` flag) so we + // cannot replace prose embedded mid-paragraph. Deliberately no `$`: text + // after the closing `)` on the same line (e.g. ". MCP tools.") stays intact. + const statsPattern = /^(?:Indexed as|indexed by GitNexus as) \*\*[^*]+\*\* \([^)]+\)/m; + + if (statsPattern.test(existingSection)) { + const updatedSection = existingSection.replace(statsPattern, statsLine); + const before = existingContent.substring(0, startIdx); + const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length); + await fs.writeFile(filePath, (before + updatedSection + after).trim() + '\n', 'utf-8'); + return 'updated'; + } + // Keep marker present but no stats line matched. Section is preserved + // unchanged on disk; return a distinct status so callers/CLI output + // don't mis-report this as 'updated' (which would imply a write). + return 'preserved'; + } + + // No keep marker — replace existing section with full verbose content const before = existingContent.substring(0, startIdx); const after = existingContent.substring(endIdx + GITNEXUS_END_MARKER.length); const newContent = before + content + after; @@ -344,12 +389,12 @@ export async function generateAIContextFiles( if (!options?.skipAgentsMd) { // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.) const agentsPath = path.join(repoPath, 'AGENTS.md'); - const agentsResult = await upsertGitNexusSection(agentsPath, content); + const agentsResult = await upsertGitNexusSection(agentsPath, content, projectName, stats); createdFiles.push(`AGENTS.md (${agentsResult})`); // Create CLAUDE.md (for Claude Code) const claudePath = path.join(repoPath, 'CLAUDE.md'); - const claudeResult = await upsertGitNexusSection(claudePath, content); + const claudeResult = await upsertGitNexusSection(claudePath, content, projectName, stats); createdFiles.push(`CLAUDE.md (${claudeResult})`); } else { createdFiles.push('AGENTS.md (skipped via --skip-agents-md)'); diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index d5a7638f9..a20503bc4 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -117,8 +117,18 @@ export interface AnalyzeOptions { verbose?: boolean; /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */ skipAgentsMd?: boolean; - /** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */ - noStats?: boolean; + /** + * Stats inclusion in AGENTS.md and CLAUDE.md. + * + * Commander.js represents `--no-stats` as `stats: boolean` (default + * `true`; `false` when the user passes `--no-stats`), NOT as + * `noStats: boolean`. Reading the negated form would always be + * `undefined` and the flag would silently no-op (#1477). Consumers + * that want "did the user request --no-stats?" should compare with + * `=== false` to distinguish the explicit-off case from the + * default-on case. + */ + stats?: boolean; /** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */ skipSkills?: boolean; /** Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills). */ @@ -449,7 +459,12 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption skipGit: options?.skipGit, skipAgentsMd, skipSkills, - noStats: options?.noStats, + // commander.js `.option('--no-stats', …)` registers the flag as + // `options.stats` (boolean, default true; `false` when the user + // passed --no-stats). Reading `options?.noStats` here returns + // undefined every time, so the flag was a no-op on the markdown + // rewrite path before this fix. See #1477. + noStats: options?.stats === false, registryName: options?.name, // Registry-collision bypass — its own CLI flag, intentionally NOT // overloading --force. A user who hits the collision guard should @@ -537,7 +552,13 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption processes: s.processes, }, skillResult.skills, - { skipAgentsMd, skipSkills, noStats: options?.noStats }, + { + skipAgentsMd, + skipSkills, + // Mirror runFullAnalysis `noStats` bridge (#1477) — same expression; + // exercised on the `--skills` path by analyze-no-stats-bridge.test.ts. + noStats: options?.stats === false, + }, ); } } catch { diff --git a/gitnexus/test/unit/ai-context.test.ts b/gitnexus/test/unit/ai-context.test.ts index 71d7ddbdc..68dee21dd 100644 --- a/gitnexus/test/unit/ai-context.test.ts +++ b/gitnexus/test/unit/ai-context.test.ts @@ -45,6 +45,54 @@ describe('generateAIContextFiles', () => { expect(content).toContain('TestProject'); }); + it('omits volatile counts when noStats option is set (#1477)', async () => { + // Distinct subdir per case so we can assert on a clean slate. + const subDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-no-stats-test-')); + const subStorage = path.join(subDir, '.gitnexus'); + await fs.mkdir(subStorage, { recursive: true }); + try { + // Stats values picked to be unmistakable if they leak through. + const stats = { nodes: 12345, edges: 67890, processes: 99 }; + await generateAIContextFiles(subDir, subStorage, 'NoStatsProject', stats, undefined, { + noStats: true, + }); + + for (const f of ['CLAUDE.md', 'AGENTS.md']) { + const content = await fs.readFile(path.join(subDir, f), 'utf-8'); + expect(content).toContain('NoStatsProject'); + // The "(N symbols, N relationships, N execution flows)" + // phrase MUST NOT appear when noStats=true. + expect(content).not.toMatch( + /\(\d+\s+symbols,\s+\d+\s+relationships,\s+\d+\s+execution flows\)/, + ); + // And the distinctive numbers must not leak via any other path. + expect(content).not.toContain('12345'); + expect(content).not.toContain('67890'); + } + } finally { + await fs.rm(subDir, { recursive: true, force: true }); + } + }); + + it('preserves volatile counts when noStats is not set (default)', async () => { + const subDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-with-stats-test-')); + const subStorage = path.join(subDir, '.gitnexus'); + await fs.mkdir(subStorage, { recursive: true }); + try { + const stats = { nodes: 12345, edges: 67890, processes: 99 }; + await generateAIContextFiles(subDir, subStorage, 'WithStatsProject', stats); + for (const f of ['CLAUDE.md', 'AGENTS.md']) { + const content = await fs.readFile(path.join(subDir, f), 'utf-8'); + expect(content).toContain('WithStatsProject'); + expect(content).toMatch( + /\(12345\s+symbols,\s+67890\s+relationships,\s+99\s+execution flows\)/, + ); + } + } finally { + await fs.rm(subDir, { recursive: true, force: true }); + } + }); + it('keeps the load-bearing repo-specific sections in the CLAUDE.md block (#856)', async () => { // The trimmed block must still contain everything that is genuinely // unique per repo or load-bearing for the agent: the freshness warning, @@ -123,9 +171,81 @@ describe('generateAIContextFiles', () => { expect(starts).toBe(1); }); + it('preserves custom section when gitnexus:keep is present', async () => { + const claudeMdPath = path.join(tmpDir, 'CLAUDE.md'); + + // Write a custom lean section with keep marker + const customContent = `# My Project + +Some project docs here. + + + +# GitNexus — Code Knowledge Graph + +Indexed as **TestProject** (50 symbols, 100 relationships, 5 execution flows). MCP tools. + +| Tool | Use for | +|------|---------| +| query | Find flows | + +Resources: gitnexus://repo/TestProject/context + +`; + await fs.writeFile(claudeMdPath, customContent, 'utf-8'); + + // Run analyze with new stats — should only update the stats line + const stats = { nodes: 999, edges: 1234, processes: 42 }; + await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats); + + const result = await fs.readFile(claudeMdPath, 'utf-8'); + + // Stats should be updated + expect(result).toContain('999 symbols'); + expect(result).toContain('1234 relationships'); + expect(result).toContain('42 execution flows'); + expect(result).toContain('. MCP tools.'); + + // Custom layout should be preserved (not replaced with verbose template) + expect(result).toContain(''); + expect(result).toContain('Code Knowledge Graph'); + expect(result).toContain('| query | Find flows |'); + + // Verbose template sections should NOT be present + expect(result).not.toContain('## Always Do'); + expect(result).not.toContain('## Never Do'); + expect(result).not.toContain('## When Debugging'); + + // Non-GitNexus content should be preserved + expect(result).toContain('# My Project'); + expect(result).toContain('Some project docs here.'); + }); + + it('replaces section when no keep marker is present', async () => { + const agentsPath = path.join(tmpDir, 'AGENTS.md'); + + // Write a section WITHOUT keep marker + const content = ` +# GitNexus — Code Intelligence + +Old content here. + +`; + await fs.writeFile(agentsPath, content, 'utf-8'); + + const stats = { nodes: 100, edges: 200, processes: 10 }; + await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats); + + const result = await fs.readFile(agentsPath, 'utf-8'); + + // Should have the full verbose template + expect(result).toContain('## Always Do'); + expect(result).not.toContain('Old content here'); + }); + it('installs skills files', async () => { const stats = { nodes: 10 }; - const result = await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats); + await generateAIContextFiles(tmpDir, storagePath, 'TestProject', stats); // Should have installed skill files const skillsDir = path.join(tmpDir, '.claude', 'skills', 'gitnexus'); @@ -371,4 +491,240 @@ describe('generateAIContextFiles', () => { await fs.rm(crlfDir, { recursive: true, force: true }); } }); + + // ────────────────────────────────────────────────────────────────── + // Keep-marker edge cases (added to address PR #1508 review findings) + // ────────────────────────────────────────────────────────────────── + + it('keep marker OUTSIDE the GitNexus section has no effect (#1508 review F5)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-scope-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + // Keep marker appears in user prose BEFORE the GitNexus section. + // The keep-path must NOT be triggered — full template replacement + // is the correct behavior here, because the marker is not inside + // the generated block. + const fileWithOutOfBandMarker = `# My Project + +A note about markers: they only apply inside the +GitNexus block below, not in prose like this. + + +Old verbose stub here. + +`; + await fs.writeFile(claudePath, fileWithOutOfBandMarker, 'utf-8'); + + const stats = { nodes: 50, edges: 100, processes: 5 }; + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), 'TestProject', stats); + + const result = await fs.readFile(claudePath, 'utf-8'); + // Section MUST have been fully replaced — keep marker outside section ignored + expect(result).toContain('## Always Do'); + expect(result).not.toContain('Old verbose stub here.'); + // User's prose with the marker reference is preserved untouched + expect(result).toContain('A note about markers'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('AGENTS.md keep path preserves custom layout (#1508 review F5)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-agents-')); + try { + const agentsPath = path.join(dir, 'AGENTS.md'); + const customAgents = `# AGENTS instructions + +Project-specific agent guidance. + + + +# GitNexus context for AGENTS + +Indexed as **AgentsTest** (10 symbols, 20 relationships, 1 execution flows). + +Use 'query' for finding flows, 'context' for symbol details. + +`; + await fs.writeFile(agentsPath, customAgents, 'utf-8'); + + const stats = { nodes: 777, edges: 888, processes: 9 }; + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), 'AgentsTest', stats); + + const result = await fs.readFile(agentsPath, 'utf-8'); + // Stats updated + expect(result).toContain('777 symbols'); + expect(result).toContain('888 relationships'); + expect(result).toContain('9 execution flows'); + // Custom layout preserved + expect(result).toContain('# GitNexus context for AGENTS'); + expect(result).toContain("Use 'query' for finding flows"); + // Verbose template NOT injected + expect(result).not.toContain('## Always Do'); + // Non-GitNexus content preserved + expect(result).toContain('# AGENTS instructions'); + expect(result).toContain('Project-specific agent guidance.'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('idempotent: second run with keep marker produces byte-identical output (#1508 review F5)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-idem-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + const seed = `# Project + + + +Indexed as **Idem** (1 symbols, 2 relationships, 3 execution flows). Custom. + +`; + await fs.writeFile(claudePath, seed, 'utf-8'); + + const stats = { nodes: 99, edges: 100, processes: 7 }; + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), 'Idem', stats); + const afterFirst = await fs.readFile(claudePath, 'utf-8'); + + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), 'Idem', stats); + const afterSecond = await fs.readFile(claudePath, 'utf-8'); + + expect(afterSecond).toBe(afterFirst); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('CRLF file with keep marker: stats line updates without corrupting content (#1508 review F5)', async () => { + // upsertGitNexusSection writes with .trim() + '\n', so the saved file uses LF + // line endings throughout — CRLF in the seed input is not preserved. + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-crlf-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + const crlfContent = + '# Project\r\n' + + '\r\n' + + '\r\n' + + '\r\n' + + 'Indexed as **CRLFTest** (5 symbols, 6 relationships, 7 execution flows). Custom CRLF.\r\n' + + '\r\n'; + await fs.writeFile(claudePath, crlfContent, 'utf-8'); + + const stats = { nodes: 50, edges: 60, processes: 7 }; + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), 'CRLFTest', stats); + + const result = await fs.readFile(claudePath, 'utf-8'); + // Stats updated correctly + expect(result).toContain('50 symbols'); + expect(result).toContain('60 relationships'); + // Custom prose preserved + expect(result).toContain('Custom CRLF'); + // No verbose template injected + expect(result).not.toContain('## Always Do'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('noStats + keep marker: stats line update is NOT corrupted by Always-Do tuple text (#1508 review F3)', async () => { + // Regression guard: with the old fallback regex `\(([^)]+)\)`, when + // noStats=true suppressed the canonical stats line from generated + // content, the fallback matched the FIRST parenthesized text in the + // template, which was `({target: "symbolName", direction: "upstream"})` + // from the Always Do bullet — silently writing that as the stats line. + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-nostats-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + const seed = ` + +Indexed as **NoStatsTest** (1 symbols, 1 relationships, 1 execution flows). Custom. + +`; + await fs.writeFile(claudePath, seed, 'utf-8'); + + const stats = { nodes: 42, edges: 84, processes: 3 }; + await generateAIContextFiles( + dir, + path.join(dir, '.gitnexus'), + 'NoStatsTest', + stats, + undefined, + { noStats: true }, + ); + + const result = await fs.readFile(claudePath, 'utf-8'); + // Stats line MUST NOT have been corrupted with the Always-Do tuple text + expect(result).not.toMatch(/\(\{target:/); + expect(result).not.toMatch(/direction:\s*"upstream"/); + // Stats line should reflect a sensible numeric update (passed stats) + expect(result).toContain('42 symbols'); + // Custom prose still preserved + expect(result).toContain('Custom.'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it("returns 'preserved' (not 'updated') when keep marker is present but no stats line matches (#1508 review F1)", async () => { + // Regression guard for the misleading-return-value bug: previously the + // function returned 'updated' without writing when the keep-section had + // no recognizable stats line, causing CLI output to claim files were + // updated when they were not. + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-noline-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + // Custom keep-section with NO "Indexed as ..." or "indexed by GitNexus as ..." line + const seed = `# Project + + + +# GitNexus block (custom, no stats line) + +This block intentionally omits the standard stats line. + +`; + await fs.writeFile(claudePath, seed, 'utf-8'); + + const stats = { nodes: 100, edges: 200, processes: 10 }; + const result = await generateAIContextFiles( + dir, + path.join(dir, '.gitnexus'), + 'NoLineTest', + stats, + ); + + // The result manifest should reflect 'preserved', not 'updated' + expect(result.files).toContain('CLAUDE.md (preserved)'); + // File on disk is unchanged + const onDisk = await fs.readFile(claudePath, 'utf-8'); + expect(onDisk).toBe(seed); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('project name with markdown-sensitive punctuation lands intact in stats line (#1508 review F5)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-keep-punct-')); + try { + const claudePath = path.join(dir, 'CLAUDE.md'); + const seed = ` + +Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Custom. + +`; + await fs.writeFile(claudePath, seed, 'utf-8'); + + // Name with hyphens, dot, and slash — exactly what dp-web4/some-repo + // style names look like + const trickyName = 'dp-web4/some-repo.v2'; + const stats = { nodes: 5, edges: 10, processes: 1 }; + await generateAIContextFiles(dir, path.join(dir, '.gitnexus'), trickyName, stats); + + const result = await fs.readFile(claudePath, 'utf-8'); + // The full name appears in the bold of the stats line, intact + expect(result).toContain(`Indexed as **${trickyName}** (5 symbols`); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/gitnexus/test/unit/analyze-no-stats-bridge.test.ts b/gitnexus/test/unit/analyze-no-stats-bridge.test.ts new file mode 100644 index 000000000..f941141ef --- /dev/null +++ b/gitnexus/test/unit/analyze-no-stats-bridge.test.ts @@ -0,0 +1,140 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock } = vi.hoisted( + () => { + const runFullAnalysisMock = vi.fn(); + const generateAIContextFilesMock = vi.fn(async () => ({ files: [] as string[] })); + const generateSkillFilesMock = vi.fn(async () => ({ + skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }], + outputPath: '/repo/.claude/skills/generated', + })); + return { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock }; + }, +); + +vi.mock('../../src/core/run-analyze.js', () => ({ + runFullAnalysis: runFullAnalysisMock, +})); + +vi.mock('../../src/cli/ai-context.js', () => ({ + generateAIContextFiles: generateAIContextFilesMock, +})); + +vi.mock('../../src/cli/skill-gen.js', () => ({ + generateSkillFiles: generateSkillFilesMock, +})); + +vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({ + closeLbug: vi.fn(async () => undefined), +})); + +vi.mock('../../src/storage/repo-manager.js', () => ({ + getStoragePaths: vi.fn(() => ({ storagePath: '.gitnexus', lbugPath: '.gitnexus/lbug' })), + getGlobalRegistryPath: vi.fn(() => 'registry.json'), + RegistryNameCollisionError: class RegistryNameCollisionError extends Error {}, + AnalysisNotFinalizedError: class AnalysisNotFinalizedError extends Error {}, + assertAnalysisFinalized: vi.fn(async () => undefined), +})); + +vi.mock('../../src/storage/git.js', () => ({ + getGitRoot: vi.fn(() => '/repo'), + hasGitDir: vi.fn(() => true), +})); + +vi.mock('../../src/core/ingestion/utils/max-file-size.js', () => ({ + getMaxFileSizeBannerMessage: vi.fn(() => null), +})); + +describe('analyzeCommand commander → runFullAnalysis noStats bridge (#1477)', () => { + beforeEach(() => { + vi.resetModules(); + runFullAnalysisMock.mockReset(); + runFullAnalysisMock.mockResolvedValue({ + repoName: 'repo', + repoPath: '/repo', + stats: {}, + alreadyUpToDate: true, + }); + generateAIContextFilesMock.mockReset(); + generateAIContextFilesMock.mockResolvedValue({ files: [] }); + generateSkillFilesMock.mockReset(); + generateSkillFilesMock.mockResolvedValue({ + skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }], + outputPath: '/repo/.claude/skills/generated', + }); + process.exitCode = undefined; + process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim(); + }); + + it('maps commander-shaped stats:false to noStats:true (equivalent to --no-stats)', async () => { + const { analyzeCommand } = await import('../../src/cli/analyze.js'); + + await analyzeCommand(undefined, { stats: false }); + + expect(runFullAnalysisMock).toHaveBeenCalledTimes(1); + const opts = runFullAnalysisMock.mock.calls[0][1]; + expect(opts.noStats).toBe(true); + }); + + it('maps omitted stats to noStats:false (default-on preserved)', async () => { + const { analyzeCommand } = await import('../../src/cli/analyze.js'); + + await analyzeCommand(undefined, {}); + + const opts = runFullAnalysisMock.mock.calls[0][1]; + expect(opts.noStats).toBe(false); + }); + + it('maps explicit stats:true to noStats:false', async () => { + const { analyzeCommand } = await import('../../src/cli/analyze.js'); + + await analyzeCommand(undefined, { stats: true }); + + const opts = runFullAnalysisMock.mock.calls[0][1]; + expect(opts.noStats).toBe(false); + }); + + it('still maps stats:false to noStats:true when skipAgentsMd is set', async () => { + const { analyzeCommand } = await import('../../src/cli/analyze.js'); + + await analyzeCommand(undefined, { stats: false, skipAgentsMd: true }); + + const opts = runFullAnalysisMock.mock.calls[0][1]; + expect(opts.noStats).toBe(true); + expect(opts.skipAgentsMd).toBe(true); + }); + + it('passes stats:false as noStats to generateAIContextFiles on the --skills regeneration path (#1477)', async () => { + runFullAnalysisMock.mockResolvedValueOnce({ + repoName: 'repo', + repoPath: '/repo', + stats: { + files: 1, + nodes: 10, + edges: 20, + communities: 0, + processes: 5, + }, + alreadyUpToDate: false, + pipelineResult: { communityResult: undefined }, + }); + + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + try { + const { analyzeCommand } = await import('../../src/cli/analyze.js'); + + await analyzeCommand(undefined, { skills: true, stats: false }); + + expect(generateSkillFilesMock).toHaveBeenCalledTimes(1); + expect(generateAIContextFilesMock).toHaveBeenCalledTimes(1); + const aiCtxOpts = generateAIContextFilesMock.mock.calls[0]![5]; + expect(aiCtxOpts).toEqual({ + skipAgentsMd: undefined, + skipSkills: undefined, + noStats: true, + }); + } finally { + exitSpy.mockRestore(); + } + }); +});