codesift-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -23
- package/dist/cache/hono-cache.d.ts +50 -0
- package/dist/cache/hono-cache.d.ts.map +1 -0
- package/dist/cache/hono-cache.js +132 -0
- package/dist/cache/hono-cache.js.map +1 -0
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +8 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/platform.d.ts.map +1 -1
- package/dist/cli/platform.js +12 -14
- package/dist/cli/platform.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +27 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/formatters-shortening.d.ts +13 -0
- package/dist/formatters-shortening.d.ts.map +1 -1
- package/dist/formatters-shortening.js +131 -0
- package/dist/formatters-shortening.js.map +1 -1
- package/dist/formatters.d.ts +38 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/formatters.js +521 -0
- package/dist/formatters.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +39 -38
- package/dist/instructions.js.map +1 -1
- package/dist/lsp/lsp-servers.d.ts.map +1 -1
- package/dist/lsp/lsp-servers.js +5 -0
- package/dist/lsp/lsp-servers.js.map +1 -1
- package/dist/lsp/lsp-tools.d.ts.map +1 -1
- package/dist/lsp/lsp-tools.js +1 -0
- package/dist/lsp/lsp-tools.js.map +1 -1
- package/dist/parser/astro-template.d.ts +47 -0
- package/dist/parser/astro-template.d.ts.map +1 -0
- package/dist/parser/astro-template.js +171 -0
- package/dist/parser/astro-template.js.map +1 -0
- package/dist/parser/extractors/_shared.d.ts +4 -0
- package/dist/parser/extractors/_shared.d.ts.map +1 -1
- package/dist/parser/extractors/_shared.js +8 -0
- package/dist/parser/extractors/_shared.js.map +1 -1
- package/dist/parser/extractors/astro.d.ts +4 -5
- package/dist/parser/extractors/astro.d.ts.map +1 -1
- package/dist/parser/extractors/astro.js +102 -26
- package/dist/parser/extractors/astro.js.map +1 -1
- package/dist/parser/extractors/gradle-kts.d.ts +4 -0
- package/dist/parser/extractors/gradle-kts.d.ts.map +1 -0
- package/dist/parser/extractors/gradle-kts.js +246 -0
- package/dist/parser/extractors/gradle-kts.js.map +1 -0
- package/dist/parser/extractors/hono-inline-analyzer.d.ts +34 -0
- package/dist/parser/extractors/hono-inline-analyzer.d.ts.map +1 -0
- package/dist/parser/extractors/hono-inline-analyzer.js +465 -0
- package/dist/parser/extractors/hono-inline-analyzer.js.map +1 -0
- package/dist/parser/extractors/hono-model.d.ts +196 -0
- package/dist/parser/extractors/hono-model.d.ts.map +1 -0
- package/dist/parser/extractors/hono-model.js +10 -0
- package/dist/parser/extractors/hono-model.js.map +1 -0
- package/dist/parser/extractors/hono.d.ts +118 -0
- package/dist/parser/extractors/hono.d.ts.map +1 -0
- package/dist/parser/extractors/hono.js +1527 -0
- package/dist/parser/extractors/hono.js.map +1 -0
- package/dist/parser/extractors/kotlin.d.ts +4 -0
- package/dist/parser/extractors/kotlin.d.ts.map +1 -0
- package/dist/parser/extractors/kotlin.js +521 -0
- package/dist/parser/extractors/kotlin.js.map +1 -0
- package/dist/parser/extractors/php.d.ts +22 -0
- package/dist/parser/extractors/php.d.ts.map +1 -0
- package/dist/parser/extractors/php.js +334 -0
- package/dist/parser/extractors/php.js.map +1 -0
- package/dist/parser/extractors/python.d.ts.map +1 -1
- package/dist/parser/extractors/python.js +234 -11
- package/dist/parser/extractors/python.js.map +1 -1
- package/dist/parser/extractors/sql.d.ts +33 -0
- package/dist/parser/extractors/sql.d.ts.map +1 -0
- package/dist/parser/extractors/sql.js +506 -0
- package/dist/parser/extractors/sql.js.map +1 -0
- package/dist/parser/extractors/typescript.d.ts.map +1 -1
- package/dist/parser/extractors/typescript.js +209 -3
- package/dist/parser/extractors/typescript.js.map +1 -1
- package/dist/parser/languages/tree-sitter-javascript.wasm +0 -0
- package/dist/parser/languages/tree-sitter-kotlin.wasm +0 -0
- package/dist/parser/languages/tree-sitter-php.wasm +0 -0
- package/dist/parser/languages/tree-sitter-php_only.wasm +0 -0
- package/dist/parser/languages/tree-sitter-python.wasm +0 -0
- package/dist/parser/parse-cache.d.ts +39 -0
- package/dist/parser/parse-cache.d.ts.map +1 -0
- package/dist/parser/parse-cache.js +87 -0
- package/dist/parser/parse-cache.js.map +1 -0
- package/dist/parser/parser-manager.d.ts +32 -0
- package/dist/parser/parser-manager.d.ts.map +1 -1
- package/dist/parser/parser-manager.js +93 -3
- package/dist/parser/parser-manager.js.map +1 -1
- package/dist/parser/symbol-extractor.d.ts.map +1 -1
- package/dist/parser/symbol-extractor.js +16 -0
- package/dist/parser/symbol-extractor.js.map +1 -1
- package/dist/register-tools.d.ts +38 -2
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +2444 -195
- package/dist/register-tools.js.map +1 -1
- package/dist/search/reranker.js +1 -1
- package/dist/search/reranker.js.map +1 -1
- package/dist/search/tool-ranker.d.ts +90 -0
- package/dist/search/tool-ranker.d.ts.map +1 -0
- package/dist/search/tool-ranker.js +420 -0
- package/dist/search/tool-ranker.js.map +1 -0
- package/dist/server-helpers.d.ts.map +1 -1
- package/dist/server-helpers.js +11 -0
- package/dist/server-helpers.js.map +1 -1
- package/dist/server.js +47 -14
- package/dist/server.js.map +1 -1
- package/dist/storage/index-store.d.ts +15 -1
- package/dist/storage/index-store.d.ts.map +1 -1
- package/dist/storage/index-store.js +27 -1
- package/dist/storage/index-store.js.map +1 -1
- package/dist/storage/session-state.d.ts +1 -1
- package/dist/storage/session-state.d.ts.map +1 -1
- package/dist/storage/session-state.js +6 -4
- package/dist/storage/session-state.js.map +1 -1
- package/dist/storage/usage-tracker.d.ts.map +1 -1
- package/dist/storage/usage-tracker.js +4 -1
- package/dist/storage/usage-tracker.js.map +1 -1
- package/dist/tools/agent-config-tools.d.ts +24 -0
- package/dist/tools/agent-config-tools.d.ts.map +1 -0
- package/dist/tools/agent-config-tools.js +119 -0
- package/dist/tools/agent-config-tools.js.map +1 -0
- package/dist/tools/architecture-tools.d.ts +23 -0
- package/dist/tools/architecture-tools.d.ts.map +1 -0
- package/dist/tools/architecture-tools.js +140 -0
- package/dist/tools/architecture-tools.js.map +1 -0
- package/dist/tools/astro-actions.d.ts +54 -0
- package/dist/tools/astro-actions.d.ts.map +1 -0
- package/dist/tools/astro-actions.js +561 -0
- package/dist/tools/astro-actions.js.map +1 -0
- package/dist/tools/astro-audit.d.ts +87 -0
- package/dist/tools/astro-audit.d.ts.map +1 -0
- package/dist/tools/astro-audit.js +345 -0
- package/dist/tools/astro-audit.js.map +1 -0
- package/dist/tools/astro-config.d.ts +33 -0
- package/dist/tools/astro-config.d.ts.map +1 -0
- package/dist/tools/astro-config.js +260 -0
- package/dist/tools/astro-config.js.map +1 -0
- package/dist/tools/astro-content-collections.d.ts +44 -0
- package/dist/tools/astro-content-collections.d.ts.map +1 -0
- package/dist/tools/astro-content-collections.js +630 -0
- package/dist/tools/astro-content-collections.js.map +1 -0
- package/dist/tools/astro-islands.d.ts +63 -0
- package/dist/tools/astro-islands.d.ts.map +1 -0
- package/dist/tools/astro-islands.js +255 -0
- package/dist/tools/astro-islands.js.map +1 -0
- package/dist/tools/astro-migration.d.ts +31 -0
- package/dist/tools/astro-migration.d.ts.map +1 -0
- package/dist/tools/astro-migration.js +378 -0
- package/dist/tools/astro-migration.js.map +1 -0
- package/dist/tools/astro-routes.d.ts +49 -0
- package/dist/tools/astro-routes.d.ts.map +1 -0
- package/dist/tools/astro-routes.js +119 -0
- package/dist/tools/astro-routes.js.map +1 -0
- package/dist/tools/async-correctness.d.ts +26 -0
- package/dist/tools/async-correctness.d.ts.map +1 -0
- package/dist/tools/async-correctness.js +166 -0
- package/dist/tools/async-correctness.js.map +1 -0
- package/dist/tools/audit-tools.d.ts +38 -0
- package/dist/tools/audit-tools.d.ts.map +1 -0
- package/dist/tools/audit-tools.js +248 -0
- package/dist/tools/audit-tools.js.map +1 -0
- package/dist/tools/celery-tools.d.ts +38 -0
- package/dist/tools/celery-tools.d.ts.map +1 -0
- package/dist/tools/celery-tools.js +154 -0
- package/dist/tools/celery-tools.js.map +1 -0
- package/dist/tools/clone-tools.js +1 -1
- package/dist/tools/clone-tools.js.map +1 -1
- package/dist/tools/complexity-tools.d.ts +4 -0
- package/dist/tools/complexity-tools.d.ts.map +1 -1
- package/dist/tools/complexity-tools.js +78 -4
- package/dist/tools/complexity-tools.js.map +1 -1
- package/dist/tools/compose-tools.d.ts +60 -0
- package/dist/tools/compose-tools.d.ts.map +1 -0
- package/dist/tools/compose-tools.js +203 -0
- package/dist/tools/compose-tools.js.map +1 -0
- package/dist/tools/coupling-tools.d.ts +50 -0
- package/dist/tools/coupling-tools.d.ts.map +1 -0
- package/dist/tools/coupling-tools.js +262 -0
- package/dist/tools/coupling-tools.js.map +1 -0
- package/dist/tools/dependency-audit-tools.d.ts +65 -0
- package/dist/tools/dependency-audit-tools.d.ts.map +1 -0
- package/dist/tools/dependency-audit-tools.js +553 -0
- package/dist/tools/dependency-audit-tools.js.map +1 -0
- package/dist/tools/django-settings.d.ts +22 -0
- package/dist/tools/django-settings.d.ts.map +1 -0
- package/dist/tools/django-settings.js +301 -0
- package/dist/tools/django-settings.js.map +1 -0
- package/dist/tools/django-view-security-tools.d.ts +32 -0
- package/dist/tools/django-view-security-tools.d.ts.map +1 -0
- package/dist/tools/django-view-security-tools.js +184 -0
- package/dist/tools/django-view-security-tools.js.map +1 -0
- package/dist/tools/fastapi-depends.d.ts +63 -0
- package/dist/tools/fastapi-depends.d.ts.map +1 -0
- package/dist/tools/fastapi-depends.js +191 -0
- package/dist/tools/fastapi-depends.js.map +1 -0
- package/dist/tools/frequency-tools.js +1 -1
- package/dist/tools/frequency-tools.js.map +1 -1
- package/dist/tools/graph-tools.d.ts +8 -2
- package/dist/tools/graph-tools.d.ts.map +1 -1
- package/dist/tools/graph-tools.js +44 -3
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/hilt-tools.d.ts +55 -0
- package/dist/tools/hilt-tools.d.ts.map +1 -0
- package/dist/tools/hilt-tools.js +258 -0
- package/dist/tools/hilt-tools.js.map +1 -0
- package/dist/tools/hono-analyze-app.d.ts +48 -0
- package/dist/tools/hono-analyze-app.d.ts.map +1 -0
- package/dist/tools/hono-analyze-app.js +94 -0
- package/dist/tools/hono-analyze-app.js.map +1 -0
- package/dist/tools/hono-api-contract.d.ts +22 -0
- package/dist/tools/hono-api-contract.d.ts.map +1 -0
- package/dist/tools/hono-api-contract.js +112 -0
- package/dist/tools/hono-api-contract.js.map +1 -0
- package/dist/tools/hono-conditional-middleware.d.ts +27 -0
- package/dist/tools/hono-conditional-middleware.d.ts.map +1 -0
- package/dist/tools/hono-conditional-middleware.js +62 -0
- package/dist/tools/hono-conditional-middleware.js.map +1 -0
- package/dist/tools/hono-context-flow.d.ts +24 -0
- package/dist/tools/hono-context-flow.d.ts.map +1 -0
- package/dist/tools/hono-context-flow.js +70 -0
- package/dist/tools/hono-context-flow.js.map +1 -0
- package/dist/tools/hono-dead-routes.d.ts +26 -0
- package/dist/tools/hono-dead-routes.d.ts.map +1 -0
- package/dist/tools/hono-dead-routes.js +102 -0
- package/dist/tools/hono-dead-routes.js.map +1 -0
- package/dist/tools/hono-entry-resolver.d.ts +27 -0
- package/dist/tools/hono-entry-resolver.d.ts.map +1 -0
- package/dist/tools/hono-entry-resolver.js +31 -0
- package/dist/tools/hono-entry-resolver.js.map +1 -0
- package/dist/tools/hono-env-regression.d.ts +29 -0
- package/dist/tools/hono-env-regression.d.ts.map +1 -0
- package/dist/tools/hono-env-regression.js +157 -0
- package/dist/tools/hono-env-regression.js.map +1 -0
- package/dist/tools/hono-inline-analyze.d.ts +31 -0
- package/dist/tools/hono-inline-analyze.d.ts.map +1 -0
- package/dist/tools/hono-inline-analyze.js +59 -0
- package/dist/tools/hono-inline-analyze.js.map +1 -0
- package/dist/tools/hono-middleware-chain.d.ts +40 -0
- package/dist/tools/hono-middleware-chain.d.ts.map +1 -0
- package/dist/tools/hono-middleware-chain.js +121 -0
- package/dist/tools/hono-middleware-chain.js.map +1 -0
- package/dist/tools/hono-modules.d.ts +22 -0
- package/dist/tools/hono-modules.d.ts.map +1 -0
- package/dist/tools/hono-modules.js +118 -0
- package/dist/tools/hono-modules.js.map +1 -0
- package/dist/tools/hono-response-types.d.ts +37 -0
- package/dist/tools/hono-response-types.d.ts.map +1 -0
- package/dist/tools/hono-response-types.js +76 -0
- package/dist/tools/hono-response-types.js.map +1 -0
- package/dist/tools/hono-rpc-types.d.ts +21 -0
- package/dist/tools/hono-rpc-types.d.ts.map +1 -0
- package/dist/tools/hono-rpc-types.js +49 -0
- package/dist/tools/hono-rpc-types.js.map +1 -0
- package/dist/tools/hono-security.d.ts +31 -0
- package/dist/tools/hono-security.d.ts.map +1 -0
- package/dist/tools/hono-security.js +269 -0
- package/dist/tools/hono-security.js.map +1 -0
- package/dist/tools/hono-visualize.d.ts +13 -0
- package/dist/tools/hono-visualize.d.ts.map +1 -0
- package/dist/tools/hono-visualize.js +64 -0
- package/dist/tools/hono-visualize.js.map +1 -0
- package/dist/tools/hotspot-tools.d.ts.map +1 -1
- package/dist/tools/hotspot-tools.js +9 -7
- package/dist/tools/hotspot-tools.js.map +1 -1
- package/dist/tools/index-tools.d.ts +17 -0
- package/dist/tools/index-tools.d.ts.map +1 -1
- package/dist/tools/index-tools.js +210 -10
- package/dist/tools/index-tools.js.map +1 -1
- package/dist/tools/kotlin-tools.d.ts +142 -0
- package/dist/tools/kotlin-tools.d.ts.map +1 -0
- package/dist/tools/kotlin-tools.js +572 -0
- package/dist/tools/kotlin-tools.js.map +1 -0
- package/dist/tools/legacy-hono-conventions.d.ts +14 -0
- package/dist/tools/legacy-hono-conventions.d.ts.map +1 -0
- package/dist/tools/legacy-hono-conventions.js +152 -0
- package/dist/tools/legacy-hono-conventions.js.map +1 -0
- package/dist/tools/migration-lint-tools.d.ts +26 -0
- package/dist/tools/migration-lint-tools.d.ts.map +1 -0
- package/dist/tools/migration-lint-tools.js +247 -0
- package/dist/tools/migration-lint-tools.js.map +1 -0
- package/dist/tools/model-tools.d.ts +30 -0
- package/dist/tools/model-tools.d.ts.map +1 -0
- package/dist/tools/model-tools.js +145 -0
- package/dist/tools/model-tools.js.map +1 -0
- package/dist/tools/nest-ext-tools.d.ts +207 -0
- package/dist/tools/nest-ext-tools.d.ts.map +1 -0
- package/dist/tools/nest-ext-tools.js +752 -0
- package/dist/tools/nest-ext-tools.js.map +1 -0
- package/dist/tools/nest-tools.d.ts +198 -0
- package/dist/tools/nest-tools.d.ts.map +1 -0
- package/dist/tools/nest-tools.js +1142 -0
- package/dist/tools/nest-tools.js.map +1 -0
- package/dist/tools/nextjs-api-contract-readers.d.ts +14 -0
- package/dist/tools/nextjs-api-contract-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-api-contract-readers.js +204 -0
- package/dist/tools/nextjs-api-contract-readers.js.map +1 -0
- package/dist/tools/nextjs-api-contract-tools.d.ts +57 -0
- package/dist/tools/nextjs-api-contract-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-api-contract-tools.js +144 -0
- package/dist/tools/nextjs-api-contract-tools.js.map +1 -0
- package/dist/tools/nextjs-boundary-tools.d.ts +39 -0
- package/dist/tools/nextjs-boundary-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-boundary-tools.js +152 -0
- package/dist/tools/nextjs-boundary-tools.js.map +1 -0
- package/dist/tools/nextjs-component-readers.d.ts +101 -0
- package/dist/tools/nextjs-component-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-component-readers.js +287 -0
- package/dist/tools/nextjs-component-readers.js.map +1 -0
- package/dist/tools/nextjs-component-tools.d.ts +51 -0
- package/dist/tools/nextjs-component-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-component-tools.js +212 -0
- package/dist/tools/nextjs-component-tools.js.map +1 -0
- package/dist/tools/nextjs-data-flow-tools.d.ts +42 -0
- package/dist/tools/nextjs-data-flow-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-data-flow-tools.js +158 -0
- package/dist/tools/nextjs-data-flow-tools.js.map +1 -0
- package/dist/tools/nextjs-framework-audit-tools.d.ts +60 -0
- package/dist/tools/nextjs-framework-audit-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-framework-audit-tools.js +394 -0
- package/dist/tools/nextjs-framework-audit-tools.js.map +1 -0
- package/dist/tools/nextjs-link-tools.d.ts +41 -0
- package/dist/tools/nextjs-link-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-link-tools.js +157 -0
- package/dist/tools/nextjs-link-tools.js.map +1 -0
- package/dist/tools/nextjs-metadata-tools.d.ts +74 -0
- package/dist/tools/nextjs-metadata-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-metadata-tools.js +252 -0
- package/dist/tools/nextjs-metadata-tools.js.map +1 -0
- package/dist/tools/nextjs-middleware-coverage-tools.d.ts +41 -0
- package/dist/tools/nextjs-middleware-coverage-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-middleware-coverage-tools.js +88 -0
- package/dist/tools/nextjs-middleware-coverage-tools.js.map +1 -0
- package/dist/tools/nextjs-route-readers.d.ts +81 -0
- package/dist/tools/nextjs-route-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-route-readers.js +340 -0
- package/dist/tools/nextjs-route-readers.js.map +1 -0
- package/dist/tools/nextjs-route-tools.d.ts +36 -0
- package/dist/tools/nextjs-route-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-route-tools.js +175 -0
- package/dist/tools/nextjs-route-tools.js.map +1 -0
- package/dist/tools/nextjs-security-readers.d.ts +22 -0
- package/dist/tools/nextjs-security-readers.d.ts.map +1 -0
- package/dist/tools/nextjs-security-readers.js +318 -0
- package/dist/tools/nextjs-security-readers.js.map +1 -0
- package/dist/tools/nextjs-security-scoring.d.ts +15 -0
- package/dist/tools/nextjs-security-scoring.d.ts.map +1 -0
- package/dist/tools/nextjs-security-scoring.js +65 -0
- package/dist/tools/nextjs-security-scoring.js.map +1 -0
- package/dist/tools/nextjs-security-tools.d.ts +75 -0
- package/dist/tools/nextjs-security-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-security-tools.js +153 -0
- package/dist/tools/nextjs-security-tools.js.map +1 -0
- package/dist/tools/nextjs-tools.d.ts +15 -0
- package/dist/tools/nextjs-tools.d.ts.map +1 -0
- package/dist/tools/nextjs-tools.js +15 -0
- package/dist/tools/nextjs-tools.js.map +1 -0
- package/dist/tools/outline-tools.d.ts.map +1 -1
- package/dist/tools/outline-tools.js +20 -0
- package/dist/tools/outline-tools.js.map +1 -1
- package/dist/tools/pattern-tools.d.ts +8 -0
- package/dist/tools/pattern-tools.d.ts.map +1 -1
- package/dist/tools/pattern-tools.js +651 -3
- package/dist/tools/pattern-tools.js.map +1 -1
- package/dist/tools/perf-tools.d.ts +32 -0
- package/dist/tools/perf-tools.d.ts.map +1 -0
- package/dist/tools/perf-tools.js +227 -0
- package/dist/tools/perf-tools.js.map +1 -0
- package/dist/tools/php-tools.d.ts +185 -0
- package/dist/tools/php-tools.d.ts.map +1 -0
- package/dist/tools/php-tools.js +645 -0
- package/dist/tools/php-tools.js.map +1 -0
- package/dist/tools/plan-turn-tools.d.ts +89 -0
- package/dist/tools/plan-turn-tools.d.ts.map +1 -0
- package/dist/tools/plan-turn-tools.js +508 -0
- package/dist/tools/plan-turn-tools.js.map +1 -0
- package/dist/tools/prisma-schema-tools.d.ts +44 -0
- package/dist/tools/prisma-schema-tools.d.ts.map +1 -0
- package/dist/tools/prisma-schema-tools.js +358 -0
- package/dist/tools/prisma-schema-tools.js.map +1 -0
- package/dist/tools/project-tools.d.ts +116 -7
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +595 -218
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/pydantic-models.d.ts +46 -0
- package/dist/tools/pydantic-models.d.ts.map +1 -0
- package/dist/tools/pydantic-models.js +249 -0
- package/dist/tools/pydantic-models.js.map +1 -0
- package/dist/tools/pyproject-tools.d.ts +23 -0
- package/dist/tools/pyproject-tools.d.ts.map +1 -0
- package/dist/tools/pyproject-tools.js +133 -0
- package/dist/tools/pyproject-tools.js.map +1 -0
- package/dist/tools/pytest-tools.d.ts +20 -0
- package/dist/tools/pytest-tools.d.ts.map +1 -0
- package/dist/tools/pytest-tools.js +106 -0
- package/dist/tools/pytest-tools.js.map +1 -0
- package/dist/tools/python-audit.d.ts +40 -0
- package/dist/tools/python-audit.d.ts.map +1 -0
- package/dist/tools/python-audit.js +244 -0
- package/dist/tools/python-audit.js.map +1 -0
- package/dist/tools/python-callers.d.ts +28 -0
- package/dist/tools/python-callers.d.ts.map +1 -0
- package/dist/tools/python-callers.js +110 -0
- package/dist/tools/python-callers.js.map +1 -0
- package/dist/tools/python-circular-imports.d.ts +19 -0
- package/dist/tools/python-circular-imports.d.ts.map +1 -0
- package/dist/tools/python-circular-imports.js +126 -0
- package/dist/tools/python-circular-imports.js.map +1 -0
- package/dist/tools/python-constants-tools.d.ts +44 -0
- package/dist/tools/python-constants-tools.d.ts.map +1 -0
- package/dist/tools/python-constants-tools.js +525 -0
- package/dist/tools/python-constants-tools.js.map +1 -0
- package/dist/tools/python-deps-analyzer.d.ts +46 -0
- package/dist/tools/python-deps-analyzer.d.ts.map +1 -0
- package/dist/tools/python-deps-analyzer.js +227 -0
- package/dist/tools/python-deps-analyzer.js.map +1 -0
- package/dist/tools/query-tools.d.ts +23 -0
- package/dist/tools/query-tools.d.ts.map +1 -0
- package/dist/tools/query-tools.js +256 -0
- package/dist/tools/query-tools.js.map +1 -0
- package/dist/tools/react-tools.d.ts +263 -0
- package/dist/tools/react-tools.d.ts.map +1 -0
- package/dist/tools/react-tools.js +839 -0
- package/dist/tools/react-tools.js.map +1 -0
- package/dist/tools/report-tools.js +47 -0
- package/dist/tools/report-tools.js.map +1 -1
- package/dist/tools/review-diff-tools.d.ts +5 -4
- package/dist/tools/review-diff-tools.d.ts.map +1 -1
- package/dist/tools/review-diff-tools.js +157 -66
- package/dist/tools/review-diff-tools.js.map +1 -1
- package/dist/tools/room-tools.d.ts +36 -0
- package/dist/tools/room-tools.d.ts.map +1 -0
- package/dist/tools/room-tools.js +147 -0
- package/dist/tools/room-tools.js.map +1 -0
- package/dist/tools/route-tools.d.ts +27 -1
- package/dist/tools/route-tools.d.ts.map +1 -1
- package/dist/tools/route-tools.js +744 -18
- package/dist/tools/route-tools.js.map +1 -1
- package/dist/tools/ruff-tools.d.ts +32 -0
- package/dist/tools/ruff-tools.d.ts.map +1 -0
- package/dist/tools/ruff-tools.js +114 -0
- package/dist/tools/ruff-tools.js.map +1 -0
- package/dist/tools/search-ranker.d.ts.map +1 -1
- package/dist/tools/search-ranker.js +7 -0
- package/dist/tools/search-ranker.js.map +1 -1
- package/dist/tools/search-tools.d.ts +3 -2
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +16 -3
- package/dist/tools/search-tools.js.map +1 -1
- package/dist/tools/serialization-tools.d.ts +24 -0
- package/dist/tools/serialization-tools.d.ts.map +1 -0
- package/dist/tools/serialization-tools.js +156 -0
- package/dist/tools/serialization-tools.js.map +1 -0
- package/dist/tools/sql-tools.d.ts +274 -0
- package/dist/tools/sql-tools.d.ts.map +1 -0
- package/dist/tools/sql-tools.js +1160 -0
- package/dist/tools/sql-tools.js.map +1 -0
- package/dist/tools/status-tools.d.ts +10 -0
- package/dist/tools/status-tools.d.ts.map +1 -0
- package/dist/tools/status-tools.js +32 -0
- package/dist/tools/status-tools.js.map +1 -0
- package/dist/tools/symbol-tools.d.ts +19 -0
- package/dist/tools/symbol-tools.d.ts.map +1 -1
- package/dist/tools/symbol-tools.js +75 -4
- package/dist/tools/symbol-tools.js.map +1 -1
- package/dist/tools/taint-tools.d.ts +43 -0
- package/dist/tools/taint-tools.d.ts.map +1 -0
- package/dist/tools/taint-tools.js +922 -0
- package/dist/tools/taint-tools.js.map +1 -0
- package/dist/tools/test-impact-tools.d.ts +29 -0
- package/dist/tools/test-impact-tools.d.ts.map +1 -0
- package/dist/tools/test-impact-tools.js +156 -0
- package/dist/tools/test-impact-tools.js.map +1 -0
- package/dist/tools/typecheck-tools.d.ts +39 -0
- package/dist/tools/typecheck-tools.d.ts.map +1 -0
- package/dist/tools/typecheck-tools.js +191 -0
- package/dist/tools/typecheck-tools.js.map +1 -0
- package/dist/tools/wiring-tools.d.ts +19 -0
- package/dist/tools/wiring-tools.d.ts.map +1 -0
- package/dist/tools/wiring-tools.js +147 -0
- package/dist/tools/wiring-tools.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/framework-detect.d.ts +18 -2
- package/dist/utils/framework-detect.d.ts.map +1 -1
- package/dist/utils/framework-detect.js +150 -3
- package/dist/utils/framework-detect.js.map +1 -1
- package/dist/utils/import-graph.d.ts +42 -0
- package/dist/utils/import-graph.d.ts.map +1 -1
- package/dist/utils/import-graph.js +248 -9
- package/dist/utils/import-graph.js.map +1 -1
- package/dist/utils/language-detect.d.ts +21 -0
- package/dist/utils/language-detect.d.ts.map +1 -0
- package/dist/utils/language-detect.js +183 -0
- package/dist/utils/language-detect.js.map +1 -0
- package/dist/utils/nextjs-ast-readers.d.ts +44 -0
- package/dist/utils/nextjs-ast-readers.d.ts.map +1 -0
- package/dist/utils/nextjs-ast-readers.js +341 -0
- package/dist/utils/nextjs-ast-readers.js.map +1 -0
- package/dist/utils/nextjs-audit-cache.d.ts +51 -0
- package/dist/utils/nextjs-audit-cache.d.ts.map +1 -0
- package/dist/utils/nextjs-audit-cache.js +116 -0
- package/dist/utils/nextjs-audit-cache.js.map +1 -0
- package/dist/utils/nextjs-metadata-readers.d.ts +65 -0
- package/dist/utils/nextjs-metadata-readers.d.ts.map +1 -0
- package/dist/utils/nextjs-metadata-readers.js +447 -0
- package/dist/utils/nextjs-metadata-readers.js.map +1 -0
- package/dist/utils/nextjs.d.ts +42 -0
- package/dist/utils/nextjs.d.ts.map +1 -0
- package/dist/utils/nextjs.js +284 -0
- package/dist/utils/nextjs.js.map +1 -0
- package/dist/utils/python-import-resolver.d.ts +42 -0
- package/dist/utils/python-import-resolver.d.ts.map +1 -0
- package/dist/utils/python-import-resolver.js +101 -0
- package/dist/utils/python-import-resolver.js.map +1 -0
- package/dist/utils/python-imports.d.ts +28 -0
- package/dist/utils/python-imports.d.ts.map +1 -0
- package/dist/utils/python-imports.js +117 -0
- package/dist/utils/python-imports.js.map +1 -0
- package/dist/utils/react-alias.d.ts +15 -0
- package/dist/utils/react-alias.d.ts.map +1 -0
- package/dist/utils/react-alias.js +31 -0
- package/dist/utils/react-alias.js.map +1 -0
- package/dist/utils/test-file.d.ts.map +1 -1
- package/dist/utils/test-file.js +7 -0
- package/dist/utils/test-file.js.map +1 -1
- package/dist/utils/walk.d.ts +22 -0
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js +70 -2
- package/dist/utils/walk.js.map +1 -1
- package/package.json +4 -3
- package/rules/codesift.md +71 -5
- package/rules/codesift.mdc +71 -5
- package/rules/codex.md +71 -5
- package/rules/gemini.md +71 -5
- package/src/parser/languages/tree-sitter-javascript.wasm +0 -0
- package/src/parser/languages/tree-sitter-kotlin.wasm +0 -0
- package/src/parser/languages/tree-sitter-php.wasm +0 -0
- package/src/parser/languages/tree-sitter-php_only.wasm +0 -0
- package/src/parser/languages/tree-sitter-python.wasm +0 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL analysis tools — analyze_schema and trace_query.
|
|
3
|
+
* Hidden/discoverable: not in CORE_TOOL_NAMES.
|
|
4
|
+
*/
|
|
5
|
+
import { getCodeIndex } from "./index-tools.js";
|
|
6
|
+
import { searchText } from "./search-tools.js";
|
|
7
|
+
const FK_RE = /REFERENCES\s+(?:(?:"([^"]+)"|(\w+))\s*\.\s*)?(?:"([^"]+)"|(\w+))\s*\(\s*(?:"([^"]+)"|(\w+))\s*\)/gi;
|
|
8
|
+
export async function analyzeSchema(repo, options) {
|
|
9
|
+
const index = await getCodeIndex(repo);
|
|
10
|
+
if (!index) {
|
|
11
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
12
|
+
}
|
|
13
|
+
const includeColumns = options?.include_columns ?? true;
|
|
14
|
+
const filePattern = options?.file_pattern;
|
|
15
|
+
// Collect tables and views from index
|
|
16
|
+
const tables = [];
|
|
17
|
+
const views = [];
|
|
18
|
+
const warnings = [];
|
|
19
|
+
const tableSymbols = index.symbols.filter((s) => (s.kind === "table" || s.kind === "view") &&
|
|
20
|
+
(!filePattern || s.file.includes(filePattern)));
|
|
21
|
+
if (tableSymbols.length === 0) {
|
|
22
|
+
warnings.push("No SQL files indexed in this repository.");
|
|
23
|
+
return { tables, views, relationships: [], warnings };
|
|
24
|
+
}
|
|
25
|
+
// Check for duplicate table names
|
|
26
|
+
const nameCounts = new Map();
|
|
27
|
+
for (const sym of tableSymbols) {
|
|
28
|
+
nameCounts.set(sym.name, (nameCounts.get(sym.name) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
for (const [name, count] of nameCounts) {
|
|
31
|
+
if (count > 1) {
|
|
32
|
+
warnings.push(`Duplicate table/view name "${name}" found in ${count} files.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const sym of tableSymbols) {
|
|
36
|
+
if (sym.kind === "view") {
|
|
37
|
+
views.push({ name: sym.name, file: sym.file, line: sym.start_line });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const columns = [];
|
|
41
|
+
if (includeColumns) {
|
|
42
|
+
const fields = index.symbols.filter((f) => f.kind === "field" && f.parent === sym.id);
|
|
43
|
+
for (const f of fields) {
|
|
44
|
+
columns.push({ name: f.name, type: f.signature ?? "unknown" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
tables.push({
|
|
48
|
+
name: sym.name,
|
|
49
|
+
file: sym.file,
|
|
50
|
+
line: sym.start_line,
|
|
51
|
+
columns,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Extract FK relationships from column signatures + table-level constraints
|
|
55
|
+
const relationships = [];
|
|
56
|
+
for (const table of tables) {
|
|
57
|
+
// Column-level REFERENCES
|
|
58
|
+
for (const col of table.columns) {
|
|
59
|
+
FK_RE.lastIndex = 0;
|
|
60
|
+
const m = FK_RE.exec(col.type);
|
|
61
|
+
if (m) {
|
|
62
|
+
const toTable = m[3] ?? m[4] ?? m[1] ?? m[2] ?? "";
|
|
63
|
+
const toCol = m[5] ?? m[6] ?? "id";
|
|
64
|
+
let relType = "fk";
|
|
65
|
+
if (toTable === table.name) {
|
|
66
|
+
relType = "self_reference";
|
|
67
|
+
}
|
|
68
|
+
relationships.push({
|
|
69
|
+
from_table: table.name,
|
|
70
|
+
from_column: col.name,
|
|
71
|
+
to_table: toTable,
|
|
72
|
+
to_column: toCol,
|
|
73
|
+
type: relType,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Table-level FOREIGN KEY constraints: scan full table source
|
|
78
|
+
const tableSym = index.symbols.find((s) => s.kind === "table" && s.name === table.name);
|
|
79
|
+
if (tableSym?.source) {
|
|
80
|
+
const tableFkRe = /FOREIGN\s+KEY\s*\(\s*(?:"([^"]+)"|(\w+))\s*\)\s*REFERENCES\s+(?:(?:"[^"]+"|(\w+))\s*\.\s*)?(?:"([^"]+)"|(\w+))\s*\(\s*(?:"([^"]+)"|(\w+))\s*\)/gi;
|
|
81
|
+
let fkm;
|
|
82
|
+
while ((fkm = tableFkRe.exec(tableSym.source)) !== null) {
|
|
83
|
+
const fromCol = fkm[1] ?? fkm[2] ?? "";
|
|
84
|
+
const toTable = fkm[4] ?? fkm[5] ?? fkm[3] ?? "";
|
|
85
|
+
const toCol = fkm[6] ?? fkm[7] ?? "id";
|
|
86
|
+
// Avoid duplicates (column-level already caught this FK)
|
|
87
|
+
if (relationships.some((r) => r.from_table === table.name && r.from_column === fromCol && r.to_table === toTable))
|
|
88
|
+
continue;
|
|
89
|
+
relationships.push({
|
|
90
|
+
from_table: table.name,
|
|
91
|
+
from_column: fromCol,
|
|
92
|
+
to_table: toTable,
|
|
93
|
+
to_column: toCol,
|
|
94
|
+
type: toTable === table.name ? "self_reference" : "fk",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Detect circular references
|
|
100
|
+
detectCircularRefs(relationships, warnings);
|
|
101
|
+
const result = { tables, views, relationships, warnings };
|
|
102
|
+
if (options?.output_format === "mermaid") {
|
|
103
|
+
result.mermaid = generateMermaid(tables, relationships);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function detectCircularRefs(relationships, warnings) {
|
|
108
|
+
// Build adjacency map
|
|
109
|
+
const adj = new Map();
|
|
110
|
+
for (const rel of relationships) {
|
|
111
|
+
if (rel.type === "self_reference")
|
|
112
|
+
continue;
|
|
113
|
+
if (!adj.has(rel.from_table))
|
|
114
|
+
adj.set(rel.from_table, new Set());
|
|
115
|
+
adj.get(rel.from_table).add(rel.to_table);
|
|
116
|
+
}
|
|
117
|
+
// DFS cycle detection with visited set
|
|
118
|
+
const visited = new Set();
|
|
119
|
+
const inStack = new Set();
|
|
120
|
+
function dfs(node) {
|
|
121
|
+
if (inStack.has(node)) {
|
|
122
|
+
warnings.push(`Circular FK reference detected involving "${node}".`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (visited.has(node))
|
|
126
|
+
return;
|
|
127
|
+
visited.add(node);
|
|
128
|
+
inStack.add(node);
|
|
129
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
130
|
+
dfs(neighbor);
|
|
131
|
+
}
|
|
132
|
+
inStack.delete(node);
|
|
133
|
+
}
|
|
134
|
+
for (const node of adj.keys()) {
|
|
135
|
+
dfs(node);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sanitize a name for Mermaid erDiagram entities.
|
|
140
|
+
* Strips leading non-word chars (e.g. Joomla's `#__`), keeps the rest readable.
|
|
141
|
+
* If the name becomes empty, returns "anon".
|
|
142
|
+
*/
|
|
143
|
+
function mermaidSafeName(name) {
|
|
144
|
+
// Strip leading non-word chars, replace remaining non-word with _
|
|
145
|
+
const cleaned = name
|
|
146
|
+
.replace(/^[^a-zA-Z0-9]+/, "") // strip leading #, $, @, etc.
|
|
147
|
+
.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
148
|
+
return cleaned || "anon";
|
|
149
|
+
}
|
|
150
|
+
/** Sanitize a column type for Mermaid — keep the base type name only (drop size/precision). */
|
|
151
|
+
function mermaidSafeType(type) {
|
|
152
|
+
// "int(10) unsigned" → "int", "varchar(255)" → "varchar", "DECIMAL(10,2)" → "decimal"
|
|
153
|
+
const m = /^[a-zA-Z][a-zA-Z0-9_]*/.exec(type);
|
|
154
|
+
return m ? m[0].toLowerCase() : "unknown";
|
|
155
|
+
}
|
|
156
|
+
function generateMermaid(tables, relationships) {
|
|
157
|
+
const lines = ["erDiagram"];
|
|
158
|
+
for (const table of tables) {
|
|
159
|
+
const safeName = mermaidSafeName(table.name);
|
|
160
|
+
lines.push(` ${safeName} {`);
|
|
161
|
+
for (const col of table.columns) {
|
|
162
|
+
const typeName = mermaidSafeType(col.type);
|
|
163
|
+
lines.push(` ${typeName} ${mermaidSafeName(col.name)}`);
|
|
164
|
+
}
|
|
165
|
+
lines.push(" }");
|
|
166
|
+
}
|
|
167
|
+
for (const rel of relationships) {
|
|
168
|
+
const arrow = rel.type === "self_reference" ? "||--o|" : "||--o{";
|
|
169
|
+
lines.push(` ${mermaidSafeName(rel.from_table)} ${arrow} ${mermaidSafeName(rel.to_table)} : "${mermaidSafeName(rel.from_column)}"`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
export async function traceQuery(repo, options) {
|
|
174
|
+
if (!options.table?.trim()) {
|
|
175
|
+
throw new Error("table parameter is required");
|
|
176
|
+
}
|
|
177
|
+
const index = await getCodeIndex(repo);
|
|
178
|
+
if (!index) {
|
|
179
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
180
|
+
}
|
|
181
|
+
const tableName = options.table.trim();
|
|
182
|
+
const maxRefs = options.max_references ?? 500;
|
|
183
|
+
const includeOrm = options.include_orm ?? true;
|
|
184
|
+
// Find table definition
|
|
185
|
+
const tableSym = index.symbols.find((s) => (s.kind === "table" || s.kind === "view") && s.name === tableName);
|
|
186
|
+
const table_definition = tableSym
|
|
187
|
+
? { file: tableSym.file, line: tableSym.start_line, kind: tableSym.kind }
|
|
188
|
+
: null;
|
|
189
|
+
// Delegate to ripgrep-backed searchText for the fast literal scan, then
|
|
190
|
+
// post-filter in JS with an identifier-char boundary regex. Ripgrep doesn't
|
|
191
|
+
// support JS-style lookbehind, and \b fails on names starting with # or $.
|
|
192
|
+
const IDENT_CHAR = "[a-zA-Z0-9_#$@]";
|
|
193
|
+
const escaped = escapeRegex(tableName);
|
|
194
|
+
const boundaryRegex = new RegExp(`(?<!${IDENT_CHAR})${escaped}(?!${IDENT_CHAR})`, "i");
|
|
195
|
+
// Fetch a wider window so post-filter losses don't silently cap results.
|
|
196
|
+
const rgMatches = await searchText(repo, tableName, {
|
|
197
|
+
regex: false,
|
|
198
|
+
max_results: Math.max(maxRefs * 3, 500),
|
|
199
|
+
file_pattern: options.file_pattern,
|
|
200
|
+
context_lines: 0,
|
|
201
|
+
});
|
|
202
|
+
const sql_references = [];
|
|
203
|
+
let kept = 0;
|
|
204
|
+
let truncated = false;
|
|
205
|
+
for (const m of rgMatches) {
|
|
206
|
+
const text = m.content ?? "";
|
|
207
|
+
if (!boundaryRegex.test(text))
|
|
208
|
+
continue;
|
|
209
|
+
// Skip the definition line itself
|
|
210
|
+
if (tableSym && m.file === tableSym.file && m.line === tableSym.start_line)
|
|
211
|
+
continue;
|
|
212
|
+
sql_references.push({
|
|
213
|
+
file: m.file,
|
|
214
|
+
line: m.line,
|
|
215
|
+
context: text.trim().slice(0, 120),
|
|
216
|
+
type: classifyReference(text),
|
|
217
|
+
});
|
|
218
|
+
kept++;
|
|
219
|
+
if (kept >= maxRefs) {
|
|
220
|
+
truncated = true;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const warnings = [];
|
|
225
|
+
if (truncated) {
|
|
226
|
+
warnings.push(`Results truncated at max_references=${maxRefs}. Pass file_pattern or increase max_references to see more.`);
|
|
227
|
+
}
|
|
228
|
+
// ORM detection
|
|
229
|
+
const orm_references = [];
|
|
230
|
+
if (includeOrm) {
|
|
231
|
+
// Prisma detection
|
|
232
|
+
const prismaFiles = index.files.filter((f) => f.path.endsWith(".prisma"));
|
|
233
|
+
for (const pf of prismaFiles) {
|
|
234
|
+
const prismaSymbols = index.symbols.filter((s) => s.file === pf.path);
|
|
235
|
+
for (const sym of prismaSymbols) {
|
|
236
|
+
// Check @@map("tableName") in source
|
|
237
|
+
if (sym.source?.includes(`@@map("${tableName}")`)) {
|
|
238
|
+
orm_references.push({
|
|
239
|
+
file: sym.file,
|
|
240
|
+
line: sym.start_line,
|
|
241
|
+
orm: "prisma",
|
|
242
|
+
model_name: sym.name,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Check model name matching table name (case-insensitive)
|
|
246
|
+
if (sym.kind === "class" && sym.name.toLowerCase() === tableName.toLowerCase()) {
|
|
247
|
+
// Avoid duplicates
|
|
248
|
+
if (!orm_references.some((r) => r.file === sym.file && r.model_name === sym.name)) {
|
|
249
|
+
orm_references.push({
|
|
250
|
+
file: sym.file,
|
|
251
|
+
line: sym.start_line,
|
|
252
|
+
orm: "prisma",
|
|
253
|
+
model_name: sym.name,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Drizzle detection
|
|
260
|
+
const tsFiles = index.files.filter((f) => f.path.endsWith(".ts") || f.path.endsWith(".js"));
|
|
261
|
+
for (const tf of tsFiles) {
|
|
262
|
+
const tsSymbols = index.symbols.filter((s) => s.file === tf.path && s.source);
|
|
263
|
+
for (const sym of tsSymbols) {
|
|
264
|
+
if (sym.source?.includes(`pgTable("${tableName}"`) ||
|
|
265
|
+
sym.source?.includes(`mysqlTable("${tableName}"`) ||
|
|
266
|
+
sym.source?.includes(`sqliteTable("${tableName}"`)) {
|
|
267
|
+
orm_references.push({
|
|
268
|
+
file: sym.file,
|
|
269
|
+
line: sym.start_line,
|
|
270
|
+
orm: "drizzle",
|
|
271
|
+
model_name: sym.name,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Warn if ORM detected but no references found
|
|
277
|
+
if (prismaFiles.length > 0 && orm_references.length === 0) {
|
|
278
|
+
warnings.push(`ORM detected (Prisma) but no model found for table "${tableName}".`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!table_definition && sql_references.length === 0) {
|
|
282
|
+
warnings.push(`Table "${tableName}" not found in indexed SQL files. If SQL support was disabled, this is expected.`);
|
|
283
|
+
}
|
|
284
|
+
return { table_definition, sql_references, orm_references, warnings, truncated };
|
|
285
|
+
}
|
|
286
|
+
function classifyReference(line) {
|
|
287
|
+
const upper = line.toUpperCase().trim();
|
|
288
|
+
if (/^\s*ALTER\s+TABLE/i.test(upper))
|
|
289
|
+
return "ddl";
|
|
290
|
+
if (/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW/i.test(upper))
|
|
291
|
+
return "view";
|
|
292
|
+
if (/REFERENCES/i.test(upper))
|
|
293
|
+
return "fk";
|
|
294
|
+
if (/^\s*(?:SELECT|INSERT|UPDATE|DELETE)\b/i.test(upper))
|
|
295
|
+
return "dml";
|
|
296
|
+
return "dml";
|
|
297
|
+
}
|
|
298
|
+
function escapeRegex(str) {
|
|
299
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
300
|
+
}
|
|
301
|
+
export async function searchColumns(repo, options) {
|
|
302
|
+
const index = await getCodeIndex(repo);
|
|
303
|
+
if (!index) {
|
|
304
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
305
|
+
}
|
|
306
|
+
const queryLower = (options.query ?? "").toLowerCase();
|
|
307
|
+
const typeFilter = options.type?.toLowerCase();
|
|
308
|
+
const tableFilter = options.table?.toLowerCase();
|
|
309
|
+
const filePattern = options.file_pattern;
|
|
310
|
+
const maxResults = options.max_results ?? 100;
|
|
311
|
+
// Build table-id → table-name lookup (only SQL tables, not Prisma models)
|
|
312
|
+
const tableIdToName = new Map();
|
|
313
|
+
for (const sym of index.symbols) {
|
|
314
|
+
if (sym.kind !== "table")
|
|
315
|
+
continue;
|
|
316
|
+
if (filePattern && !sym.file.includes(filePattern))
|
|
317
|
+
continue;
|
|
318
|
+
tableIdToName.set(sym.id, { name: sym.name, file: sym.file });
|
|
319
|
+
}
|
|
320
|
+
// Collect field symbols whose parent is a SQL table
|
|
321
|
+
const allHits = [];
|
|
322
|
+
for (const sym of index.symbols) {
|
|
323
|
+
if (sym.kind !== "field")
|
|
324
|
+
continue;
|
|
325
|
+
if (!sym.parent)
|
|
326
|
+
continue;
|
|
327
|
+
const parent = tableIdToName.get(sym.parent);
|
|
328
|
+
if (!parent)
|
|
329
|
+
continue;
|
|
330
|
+
const name = sym.name;
|
|
331
|
+
const type = sym.signature ?? "unknown";
|
|
332
|
+
const normalized = normalizeType(type);
|
|
333
|
+
// Apply filters
|
|
334
|
+
if (queryLower && !name.toLowerCase().includes(queryLower))
|
|
335
|
+
continue;
|
|
336
|
+
if (typeFilter && normalized !== typeFilter)
|
|
337
|
+
continue;
|
|
338
|
+
if (tableFilter && !parent.name.toLowerCase().includes(tableFilter))
|
|
339
|
+
continue;
|
|
340
|
+
allHits.push({
|
|
341
|
+
name,
|
|
342
|
+
type,
|
|
343
|
+
normalized_type: normalized,
|
|
344
|
+
table: parent.name,
|
|
345
|
+
file: parent.file,
|
|
346
|
+
line: sym.start_line,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const total = allHits.length;
|
|
350
|
+
const truncated = total > maxResults;
|
|
351
|
+
const columns = truncated ? allHits.slice(0, maxResults) : allHits;
|
|
352
|
+
return { columns, total, truncated };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Per-table complexity score: column count + FK count + index count.
|
|
356
|
+
* Identifies "god tables" that need refactoring. Sorted by score desc.
|
|
357
|
+
*/
|
|
358
|
+
export async function analyzeSchemaComplexity(repo, options) {
|
|
359
|
+
const index = await getCodeIndex(repo);
|
|
360
|
+
if (!index) {
|
|
361
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
362
|
+
}
|
|
363
|
+
const filePattern = options?.file_pattern;
|
|
364
|
+
const topN = options?.top_n ?? 50;
|
|
365
|
+
const tables = index.symbols.filter((s) => {
|
|
366
|
+
if (s.kind !== "table")
|
|
367
|
+
return false;
|
|
368
|
+
if (filePattern && !s.file.includes(filePattern))
|
|
369
|
+
return false;
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
// Pre-compute: index count per table name
|
|
373
|
+
const indexCounts = new Map();
|
|
374
|
+
for (const sym of index.symbols) {
|
|
375
|
+
if (sym.kind !== "index")
|
|
376
|
+
continue;
|
|
377
|
+
// Index source typically contains "ON table_name(...)"
|
|
378
|
+
const onMatch = /\bON\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i.exec(sym.source ?? "");
|
|
379
|
+
if (onMatch) {
|
|
380
|
+
const tableName = (onMatch[1] ?? onMatch[2] ?? onMatch[3] ?? onMatch[4] ?? "").toLowerCase();
|
|
381
|
+
indexCounts.set(tableName, (indexCounts.get(tableName) ?? 0) + 1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const results = [];
|
|
385
|
+
for (const table of tables) {
|
|
386
|
+
const columns = index.symbols.filter((s) => s.kind === "field" && s.parent === table.id);
|
|
387
|
+
const column_count = columns.length;
|
|
388
|
+
// Count FK references in columns
|
|
389
|
+
let fk_count = 0;
|
|
390
|
+
for (const col of columns) {
|
|
391
|
+
if (/REFERENCES/i.test(col.signature ?? ""))
|
|
392
|
+
fk_count++;
|
|
393
|
+
}
|
|
394
|
+
const index_count = indexCounts.get(table.name.toLowerCase()) ?? 0;
|
|
395
|
+
// Weighted score: columns dominate, FKs and indexes add coupling signal
|
|
396
|
+
const score = column_count * 1.0 + fk_count * 3.0 + index_count * 1.5;
|
|
397
|
+
results.push({
|
|
398
|
+
name: table.name,
|
|
399
|
+
file: table.file,
|
|
400
|
+
line: table.start_line,
|
|
401
|
+
column_count,
|
|
402
|
+
fk_count,
|
|
403
|
+
index_count,
|
|
404
|
+
score,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
results.sort((a, b) => b.score - a.score);
|
|
408
|
+
return { tables: results.slice(0, topN) };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Scan codebase for unsafe DML patterns in SQL strings.
|
|
412
|
+
* Uses ripgrep to find DML statements, then classifies safety.
|
|
413
|
+
*/
|
|
414
|
+
export async function scanDmlSafety(repo, options) {
|
|
415
|
+
const index = await getCodeIndex(repo);
|
|
416
|
+
if (!index) {
|
|
417
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
418
|
+
}
|
|
419
|
+
const filePattern = options?.file_pattern;
|
|
420
|
+
const maxResults = options?.max_results ?? 200;
|
|
421
|
+
const findings = [];
|
|
422
|
+
const filesScanned = new Set();
|
|
423
|
+
// Pattern 1: DELETE without WHERE
|
|
424
|
+
const delMatches = await searchText(repo, "DELETE FROM", {
|
|
425
|
+
regex: false,
|
|
426
|
+
max_results: maxResults,
|
|
427
|
+
file_pattern: filePattern,
|
|
428
|
+
context_lines: 0,
|
|
429
|
+
});
|
|
430
|
+
for (const m of delMatches) {
|
|
431
|
+
filesScanned.add(m.file);
|
|
432
|
+
const text = m.content ?? "";
|
|
433
|
+
// Check if WHERE exists after DELETE FROM on the same line or nearby
|
|
434
|
+
if (!/\bWHERE\b/i.test(text)) {
|
|
435
|
+
findings.push({
|
|
436
|
+
rule: "delete-without-where",
|
|
437
|
+
severity: "high",
|
|
438
|
+
file: m.file,
|
|
439
|
+
line: m.line,
|
|
440
|
+
context: text.trim().slice(0, 120),
|
|
441
|
+
detail: `DELETE FROM without WHERE clause — may delete all rows.`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Pattern 2: UPDATE without WHERE
|
|
446
|
+
const updMatches = await searchText(repo, "UPDATE", {
|
|
447
|
+
regex: false,
|
|
448
|
+
max_results: maxResults,
|
|
449
|
+
file_pattern: filePattern,
|
|
450
|
+
context_lines: 0,
|
|
451
|
+
});
|
|
452
|
+
for (const m of updMatches) {
|
|
453
|
+
filesScanned.add(m.file);
|
|
454
|
+
const text = m.content ?? "";
|
|
455
|
+
// Must contain SET (otherwise it's not a DML UPDATE)
|
|
456
|
+
if (!/\bSET\b/i.test(text))
|
|
457
|
+
continue;
|
|
458
|
+
if (!/\bWHERE\b/i.test(text)) {
|
|
459
|
+
findings.push({
|
|
460
|
+
rule: "update-without-where",
|
|
461
|
+
severity: "high",
|
|
462
|
+
file: m.file,
|
|
463
|
+
line: m.line,
|
|
464
|
+
context: text.trim().slice(0, 120),
|
|
465
|
+
detail: `UPDATE...SET without WHERE clause — may update all rows.`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Pattern 3: SELECT * (unbounded read)
|
|
470
|
+
const selMatches = await searchText(repo, "SELECT *", {
|
|
471
|
+
regex: false,
|
|
472
|
+
max_results: maxResults,
|
|
473
|
+
file_pattern: filePattern,
|
|
474
|
+
context_lines: 0,
|
|
475
|
+
});
|
|
476
|
+
for (const m of selMatches) {
|
|
477
|
+
filesScanned.add(m.file);
|
|
478
|
+
const text = m.content ?? "";
|
|
479
|
+
// Only flag if FROM is present (actual query, not comment/string fragment)
|
|
480
|
+
if (!/\bFROM\b/i.test(text))
|
|
481
|
+
continue;
|
|
482
|
+
findings.push({
|
|
483
|
+
rule: "select-star",
|
|
484
|
+
severity: "info",
|
|
485
|
+
file: m.file,
|
|
486
|
+
line: m.line,
|
|
487
|
+
context: text.trim().slice(0, 120),
|
|
488
|
+
detail: `SELECT * — fetches all columns. Consider listing specific fields.`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
// Deduplicate: same file:line + same rule
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
const deduped = findings.filter((f) => {
|
|
494
|
+
const key = `${f.file}:${f.line}:${f.rule}`;
|
|
495
|
+
if (seen.has(key))
|
|
496
|
+
return false;
|
|
497
|
+
seen.add(key);
|
|
498
|
+
return true;
|
|
499
|
+
});
|
|
500
|
+
const by_rule = {};
|
|
501
|
+
for (const f of deduped) {
|
|
502
|
+
by_rule[f.rule] = (by_rule[f.rule] ?? 0) + 1;
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
findings: deduped,
|
|
506
|
+
summary: { total: deduped.length, by_rule, files_scanned: filesScanned.size },
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Lint SQL schema for common anti-patterns.
|
|
511
|
+
* Conservative ruleset with near-zero false positive rate:
|
|
512
|
+
* - no-primary-key: table without PRIMARY KEY (serious design smell)
|
|
513
|
+
* - wide-table: table with >20 columns (god table)
|
|
514
|
+
* - duplicate-index-name: same index name defined multiple times
|
|
515
|
+
*/
|
|
516
|
+
export async function lintSchema(repo, options) {
|
|
517
|
+
const index = await getCodeIndex(repo);
|
|
518
|
+
if (!index) {
|
|
519
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
520
|
+
}
|
|
521
|
+
const filePattern = options?.file_pattern;
|
|
522
|
+
const findings = [];
|
|
523
|
+
const warnings = [];
|
|
524
|
+
const tables = index.symbols.filter((s) => {
|
|
525
|
+
if (s.kind !== "table")
|
|
526
|
+
return false;
|
|
527
|
+
if (filePattern && !s.file.includes(filePattern))
|
|
528
|
+
return false;
|
|
529
|
+
return true;
|
|
530
|
+
});
|
|
531
|
+
if (tables.length === 0) {
|
|
532
|
+
warnings.push("No SQL tables found in this repository.");
|
|
533
|
+
return { findings, summary: { total: 0, by_rule: {} }, warnings };
|
|
534
|
+
}
|
|
535
|
+
// Rule 1: no-primary-key — table with no PK field
|
|
536
|
+
for (const table of tables) {
|
|
537
|
+
const source = table.source ?? "";
|
|
538
|
+
const hasPK = /PRIMARY\s+KEY/i.test(source) || /\bSERIAL\b/i.test(source);
|
|
539
|
+
if (!hasPK) {
|
|
540
|
+
findings.push({
|
|
541
|
+
rule: "no-primary-key",
|
|
542
|
+
severity: "warning",
|
|
543
|
+
table: table.name,
|
|
544
|
+
detail: `Table "${table.name}" has no PRIMARY KEY or SERIAL column.`,
|
|
545
|
+
file: table.file,
|
|
546
|
+
line: table.start_line,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Rule 2: wide-table — >20 columns
|
|
551
|
+
for (const table of tables) {
|
|
552
|
+
const fields = index.symbols.filter((s) => s.kind === "field" && s.parent === table.id);
|
|
553
|
+
if (fields.length > 20) {
|
|
554
|
+
findings.push({
|
|
555
|
+
rule: "wide-table",
|
|
556
|
+
severity: "warning",
|
|
557
|
+
table: table.name,
|
|
558
|
+
detail: `Table "${table.name}" has ${fields.length} columns (threshold: 20). Consider splitting.`,
|
|
559
|
+
file: table.file,
|
|
560
|
+
line: table.start_line,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Rule 3: duplicate-index-name
|
|
565
|
+
const indexNames = new Map();
|
|
566
|
+
const indexes = index.symbols.filter((s) => {
|
|
567
|
+
if (s.kind !== "index")
|
|
568
|
+
return false;
|
|
569
|
+
if (filePattern && !s.file.includes(filePattern))
|
|
570
|
+
return false;
|
|
571
|
+
return true;
|
|
572
|
+
});
|
|
573
|
+
for (const idx of indexes) {
|
|
574
|
+
const key = idx.name.toLowerCase();
|
|
575
|
+
if (indexNames.has(key)) {
|
|
576
|
+
const prev = indexNames.get(key);
|
|
577
|
+
findings.push({
|
|
578
|
+
rule: "duplicate-index-name",
|
|
579
|
+
severity: "warning",
|
|
580
|
+
table: idx.name,
|
|
581
|
+
detail: `Index "${idx.name}" defined at ${idx.file}:${idx.start_line} duplicates index at ${prev.file}:${prev.line}.`,
|
|
582
|
+
file: idx.file,
|
|
583
|
+
line: idx.start_line,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
indexNames.set(key, { file: idx.file, line: idx.start_line });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Build summary
|
|
591
|
+
const by_rule = {};
|
|
592
|
+
for (const f of findings) {
|
|
593
|
+
by_rule[f.rule] = (by_rule[f.rule] ?? 0) + 1;
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
findings,
|
|
597
|
+
summary: { total: findings.length, by_rule },
|
|
598
|
+
warnings,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const MIGRATION_PATTERNS = [
|
|
602
|
+
// Destructive (high severity)
|
|
603
|
+
{ regex: /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "DROP TABLE", category: "destructive", severity: "high", targetGroup: 1 },
|
|
604
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "DROP COLUMN", category: "destructive", severity: "high", targetGroup: 1 },
|
|
605
|
+
{ regex: /DROP\s+INDEX\s+(?:IF\s+EXISTS\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "DROP INDEX", category: "destructive", severity: "medium", targetGroup: 1 },
|
|
606
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+DROP\s+CONSTRAINT/i, operation: "DROP CONSTRAINT", category: "destructive", severity: "medium", targetGroup: 1 },
|
|
607
|
+
{ regex: /TRUNCATE\s+(?:TABLE\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "TRUNCATE", category: "destructive", severity: "high", targetGroup: 1 },
|
|
608
|
+
// Modifying (medium severity)
|
|
609
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+ADD\s+COLUMN/i, operation: "ADD COLUMN", category: "modifying", severity: "low", targetGroup: 1 },
|
|
610
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+ADD\s+(?!COLUMN)/i, operation: "ALTER TABLE ADD", category: "modifying", severity: "low", targetGroup: 1 },
|
|
611
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+ALTER\s+COLUMN/i, operation: "ALTER COLUMN", category: "modifying", severity: "medium", targetGroup: 1 },
|
|
612
|
+
{ regex: /ALTER\s+TABLE\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))\s+RENAME/i, operation: "RENAME", category: "modifying", severity: "medium", targetGroup: 1 },
|
|
613
|
+
// Additive (low severity) — these overlap with the extractor's DDL patterns
|
|
614
|
+
{ regex: /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "CREATE TABLE", category: "additive", severity: "low", targetGroup: 1 },
|
|
615
|
+
{ regex: /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "CREATE INDEX", category: "additive", severity: "low", targetGroup: 1 },
|
|
616
|
+
{ regex: /CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW\s+(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|(\w+))/i, operation: "CREATE VIEW", category: "additive", severity: "low", targetGroup: 1 },
|
|
617
|
+
];
|
|
618
|
+
function pickTarget(m, startGroup) {
|
|
619
|
+
for (let i = startGroup; i < m.length; i++) {
|
|
620
|
+
if (m[i])
|
|
621
|
+
return m[i];
|
|
622
|
+
}
|
|
623
|
+
return "(unknown)";
|
|
624
|
+
}
|
|
625
|
+
export async function diffMigrations(repo, options) {
|
|
626
|
+
const index = await getCodeIndex(repo);
|
|
627
|
+
if (!index) {
|
|
628
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
629
|
+
}
|
|
630
|
+
const filePattern = options?.file_pattern;
|
|
631
|
+
// Find .sql files, sorted by name (migration order heuristic)
|
|
632
|
+
const sqlFiles = index.files
|
|
633
|
+
.filter((f) => (f.language === "sql" || f.language === "sql-jinja"))
|
|
634
|
+
.filter((f) => !filePattern || f.path.includes(filePattern))
|
|
635
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
636
|
+
const additive = [];
|
|
637
|
+
const modifying = [];
|
|
638
|
+
const destructive = [];
|
|
639
|
+
for (const fileEntry of sqlFiles) {
|
|
640
|
+
// Read file source from symbols (each symbol has source)
|
|
641
|
+
// Or reconstruct from all symbols in this file
|
|
642
|
+
const fileSymbols = index.symbols.filter((s) => s.file === fileEntry.path);
|
|
643
|
+
// Collect all raw source lines we can access
|
|
644
|
+
const seenLines = new Set();
|
|
645
|
+
for (const sym of fileSymbols) {
|
|
646
|
+
if (!sym.source)
|
|
647
|
+
continue;
|
|
648
|
+
for (const line of sym.source.split("\n")) {
|
|
649
|
+
seenLines.add(line);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Also scan the file directly for DML patterns not captured as symbols
|
|
653
|
+
// (ALTER, DROP, TRUNCATE aren't symbols — they're imperative ops)
|
|
654
|
+
let fullSource;
|
|
655
|
+
try {
|
|
656
|
+
const { readFileSync } = await import("node:fs");
|
|
657
|
+
const { join } = await import("node:path");
|
|
658
|
+
fullSource = readFileSync(join(index.root, fileEntry.path), "utf-8");
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
// File not accessible — use symbol sources only
|
|
662
|
+
}
|
|
663
|
+
const linesToScan = fullSource
|
|
664
|
+
? fullSource.split("\n")
|
|
665
|
+
: [...seenLines];
|
|
666
|
+
for (let lineIdx = 0; lineIdx < linesToScan.length; lineIdx++) {
|
|
667
|
+
const line = linesToScan[lineIdx];
|
|
668
|
+
const trimmed = line.trim();
|
|
669
|
+
if (!trimmed || trimmed.startsWith("--"))
|
|
670
|
+
continue;
|
|
671
|
+
for (const pat of MIGRATION_PATTERNS) {
|
|
672
|
+
const m = pat.regex.exec(trimmed);
|
|
673
|
+
if (!m)
|
|
674
|
+
continue;
|
|
675
|
+
const target = pickTarget(m, pat.targetGroup);
|
|
676
|
+
// For DROP COLUMN, include table.column
|
|
677
|
+
let fullTarget = target;
|
|
678
|
+
if (pat.operation === "DROP COLUMN") {
|
|
679
|
+
const colName = pickTarget(m, 5); // groups 5-8 are the column name
|
|
680
|
+
fullTarget = `${target}.${colName}`;
|
|
681
|
+
}
|
|
682
|
+
const op = {
|
|
683
|
+
operation: pat.operation,
|
|
684
|
+
target: fullTarget,
|
|
685
|
+
severity: pat.severity,
|
|
686
|
+
file: fileEntry.path,
|
|
687
|
+
line: lineIdx + 1,
|
|
688
|
+
raw: trimmed.slice(0, 120),
|
|
689
|
+
};
|
|
690
|
+
switch (pat.category) {
|
|
691
|
+
case "additive":
|
|
692
|
+
additive.push(op);
|
|
693
|
+
break;
|
|
694
|
+
case "modifying":
|
|
695
|
+
modifying.push(op);
|
|
696
|
+
break;
|
|
697
|
+
case "destructive":
|
|
698
|
+
destructive.push(op);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
break; // first match wins per line
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
additive,
|
|
707
|
+
modifying,
|
|
708
|
+
destructive,
|
|
709
|
+
summary: {
|
|
710
|
+
additive: additive.length,
|
|
711
|
+
modifying: modifying.length,
|
|
712
|
+
destructive: destructive.length,
|
|
713
|
+
total_files: sqlFiles.length,
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Find SQL tables with zero references outside their own CREATE TABLE definition.
|
|
719
|
+
* Uses ripgrep-backed literal search per table for speed.
|
|
720
|
+
*/
|
|
721
|
+
export async function findOrphanTables(repo, options) {
|
|
722
|
+
const index = await getCodeIndex(repo);
|
|
723
|
+
if (!index) {
|
|
724
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
725
|
+
}
|
|
726
|
+
const filePattern = options?.file_pattern;
|
|
727
|
+
// Collect all SQL tables
|
|
728
|
+
const tables = index.symbols.filter((s) => {
|
|
729
|
+
if (s.kind !== "table")
|
|
730
|
+
return false;
|
|
731
|
+
if (filePattern && !s.file.includes(filePattern))
|
|
732
|
+
return false;
|
|
733
|
+
return true;
|
|
734
|
+
});
|
|
735
|
+
const orphans = [];
|
|
736
|
+
for (const table of tables) {
|
|
737
|
+
// Search for references to this table name via ripgrep
|
|
738
|
+
let rgMatches = [];
|
|
739
|
+
try {
|
|
740
|
+
rgMatches = await searchText(repo, table.name, {
|
|
741
|
+
regex: false,
|
|
742
|
+
max_results: 20,
|
|
743
|
+
context_lines: 0,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
rgMatches = [];
|
|
748
|
+
}
|
|
749
|
+
// Boundary filter + exclude the definition line itself
|
|
750
|
+
const IDENT_CHAR = "[a-zA-Z0-9_#$@]";
|
|
751
|
+
const escaped = escapeRegex(table.name);
|
|
752
|
+
const boundaryRegex = new RegExp(`(?<!${IDENT_CHAR})${escaped}(?!${IDENT_CHAR})`, "i");
|
|
753
|
+
const realRefs = rgMatches.filter((m) => {
|
|
754
|
+
const text = m.content ?? "";
|
|
755
|
+
if (!boundaryRegex.test(text))
|
|
756
|
+
return false;
|
|
757
|
+
// Exclude the CREATE TABLE definition line
|
|
758
|
+
if (m.file === table.file && m.line === table.start_line)
|
|
759
|
+
return false;
|
|
760
|
+
// Exclude lines within the CREATE TABLE body (column defs, constraints)
|
|
761
|
+
if (m.file === table.file && m.line > table.start_line && m.line <= table.end_line)
|
|
762
|
+
return false;
|
|
763
|
+
return true;
|
|
764
|
+
});
|
|
765
|
+
if (realRefs.length === 0) {
|
|
766
|
+
const columnCount = index.symbols.filter((s) => s.kind === "field" && s.parent === table.id).length;
|
|
767
|
+
orphans.push({
|
|
768
|
+
name: table.name,
|
|
769
|
+
file: table.file,
|
|
770
|
+
line: table.start_line,
|
|
771
|
+
column_count: columnCount,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
orphans,
|
|
777
|
+
total_tables: tables.length,
|
|
778
|
+
orphan_count: orphans.length,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/** Parse a Prisma model block source → fields + @@map table name */
|
|
782
|
+
function parsePrismaModel(sym) {
|
|
783
|
+
const source = sym.source ?? "";
|
|
784
|
+
const lines = source.split("\n");
|
|
785
|
+
const fields = [];
|
|
786
|
+
let tableName = camelToSnake(sym.name);
|
|
787
|
+
// @@map("table_name")
|
|
788
|
+
const mapMatch = /@@map\s*\(\s*"([^"]+)"\s*\)/.exec(source);
|
|
789
|
+
if (mapMatch)
|
|
790
|
+
tableName = mapMatch[1];
|
|
791
|
+
// Field line pattern: ` fieldName Type? @attr @attr`
|
|
792
|
+
const FIELD_RE = /^\s*(\w+)\s+(\w+)(\[\])?(\?)?\s*(.*)$/;
|
|
793
|
+
const RELATION_RE = /@relation/;
|
|
794
|
+
const MAP_ATTR_RE = /@map\s*\(\s*"([^"]+)"\s*\)/;
|
|
795
|
+
// Prisma scalar types — anything else with an uppercase first letter is a model relation
|
|
796
|
+
const SCALAR_TYPES = new Set([
|
|
797
|
+
"Int", "BigInt", "Float", "Decimal", "String", "Boolean", "DateTime", "Json", "Bytes", "Unsupported",
|
|
798
|
+
]);
|
|
799
|
+
for (const line of lines) {
|
|
800
|
+
const trimmed = line.trim();
|
|
801
|
+
// Skip model header, closing brace, comments, attributes
|
|
802
|
+
if (trimmed.startsWith("model ") || trimmed === "" || trimmed === "}")
|
|
803
|
+
continue;
|
|
804
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("@@"))
|
|
805
|
+
continue;
|
|
806
|
+
const m = FIELD_RE.exec(trimmed);
|
|
807
|
+
if (!m)
|
|
808
|
+
continue;
|
|
809
|
+
const name = m[1];
|
|
810
|
+
const type = m[2];
|
|
811
|
+
const isList = m[3] === "[]";
|
|
812
|
+
const optional = m[4] === "?";
|
|
813
|
+
const attrs = m[5] ?? "";
|
|
814
|
+
// Skip reserved keywords that aren't fields (e.g. "model", "enum")
|
|
815
|
+
if (/^(model|enum|type|view)$/.test(name))
|
|
816
|
+
continue;
|
|
817
|
+
const is_id = /@id\b/.test(attrs);
|
|
818
|
+
// A field is a relation if:
|
|
819
|
+
// 1. It has @relation attr, OR
|
|
820
|
+
// 2. It's a list (Type[]) — always a relation side, OR
|
|
821
|
+
// 3. Its type starts with uppercase AND isn't a built-in scalar (→ custom model)
|
|
822
|
+
const is_relation = RELATION_RE.test(attrs) ||
|
|
823
|
+
isList ||
|
|
824
|
+
(/^[A-Z]/.test(type) && !SCALAR_TYPES.has(type));
|
|
825
|
+
// Relations aren't real DB columns — skip for drift purposes
|
|
826
|
+
if (is_relation)
|
|
827
|
+
continue;
|
|
828
|
+
// @map("db_col") overrides the db name
|
|
829
|
+
const mapField = MAP_ATTR_RE.exec(attrs);
|
|
830
|
+
const db_name = mapField ? mapField[1] : camelToSnake(name);
|
|
831
|
+
fields.push({ name, db_name, type, optional, is_id, is_relation });
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
name: sym.name,
|
|
835
|
+
table: tableName,
|
|
836
|
+
file: sym.file,
|
|
837
|
+
line: sym.start_line,
|
|
838
|
+
fields,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function camelToSnake(s) {
|
|
842
|
+
return s
|
|
843
|
+
.replace(/([A-Z])/g, (_, c) => "_" + c.toLowerCase())
|
|
844
|
+
.replace(/^_/, "");
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Normalize a type name for cross-layer comparison.
|
|
848
|
+
* Prisma Int → int, SQL INTEGER → int, Float → float, etc.
|
|
849
|
+
*
|
|
850
|
+
* Handles signatures like "TEXT NOT NULL", "SERIAL PRIMARY KEY", "int(10) unsigned".
|
|
851
|
+
* Extracts the base type word, then groups by semantic equivalence.
|
|
852
|
+
*/
|
|
853
|
+
function normalizeType(raw) {
|
|
854
|
+
// Take the first word only (strip modifiers, constraints, size).
|
|
855
|
+
// "TEXT NOT NULL" → "text", "int(10) unsigned" → "int", "DECIMAL(10,2)" → "decimal"
|
|
856
|
+
const firstWord = /[a-zA-Z]+/.exec(raw)?.[0]?.toLowerCase() ?? "";
|
|
857
|
+
// Group equivalents
|
|
858
|
+
if (/^(int|integer|smallint|bigint|serial|bigserial|smallserial|tinyint|mediumint)$/.test(firstWord))
|
|
859
|
+
return "int";
|
|
860
|
+
if (/^(float|real|double|decimal|numeric|money)$/.test(firstWord))
|
|
861
|
+
return "float";
|
|
862
|
+
if (/^(text|varchar|char|string|nvarchar|longtext|mediumtext|tinytext)$/.test(firstWord))
|
|
863
|
+
return "string";
|
|
864
|
+
if (/^(bool|boolean|bit)$/.test(firstWord))
|
|
865
|
+
return "bool";
|
|
866
|
+
if (/^(timestamp|timestamptz|datetime|date|time|timetz)$/.test(firstWord))
|
|
867
|
+
return "datetime";
|
|
868
|
+
if (/^(json|jsonb)$/.test(firstWord))
|
|
869
|
+
return "json";
|
|
870
|
+
if (/^(uuid)$/.test(firstWord))
|
|
871
|
+
return "uuid";
|
|
872
|
+
if (/^(bytea|blob|binary|longblob|mediumblob|tinyblob)$/.test(firstWord))
|
|
873
|
+
return "bytes";
|
|
874
|
+
return firstWord || "unknown";
|
|
875
|
+
}
|
|
876
|
+
export async function analyzeSchemaDrift(repo, options) {
|
|
877
|
+
const index = await getCodeIndex(repo);
|
|
878
|
+
if (!index) {
|
|
879
|
+
throw new Error(`Repository "${repo}" not found. Run index_folder first.`);
|
|
880
|
+
}
|
|
881
|
+
const filePattern = options?.file_pattern;
|
|
882
|
+
const drifts = [];
|
|
883
|
+
const warnings = [];
|
|
884
|
+
const orms_detected = [];
|
|
885
|
+
// Collect SQL tables and fields
|
|
886
|
+
const sqlTables = new Map();
|
|
887
|
+
for (const sym of index.symbols) {
|
|
888
|
+
if (sym.kind !== "table")
|
|
889
|
+
continue;
|
|
890
|
+
if (filePattern && !sym.file.includes(filePattern))
|
|
891
|
+
continue;
|
|
892
|
+
const columns = new Map();
|
|
893
|
+
const fields = index.symbols.filter((f) => f.kind === "field" && f.parent === sym.id);
|
|
894
|
+
for (const f of fields) {
|
|
895
|
+
columns.set(f.name.toLowerCase(), {
|
|
896
|
+
type: f.signature ?? "unknown",
|
|
897
|
+
file: f.file,
|
|
898
|
+
line: f.start_line,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
sqlTables.set(sym.name.toLowerCase(), {
|
|
902
|
+
file: sym.file,
|
|
903
|
+
line: sym.start_line,
|
|
904
|
+
columns,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
// Collect Prisma models (kind === "class" in prisma extractor)
|
|
908
|
+
const prismaModels = [];
|
|
909
|
+
for (const sym of index.symbols) {
|
|
910
|
+
if (sym.kind !== "class")
|
|
911
|
+
continue;
|
|
912
|
+
if (!sym.file.endsWith(".prisma"))
|
|
913
|
+
continue;
|
|
914
|
+
if (filePattern && !sym.file.includes(filePattern))
|
|
915
|
+
continue;
|
|
916
|
+
prismaModels.push(parsePrismaModel(sym));
|
|
917
|
+
}
|
|
918
|
+
if (prismaModels.length > 0)
|
|
919
|
+
orms_detected.push("prisma");
|
|
920
|
+
// TODO: Drizzle and TypeORM collection (v1.2)
|
|
921
|
+
if (orms_detected.length === 0) {
|
|
922
|
+
warnings.push("No ORM models found in repository. analyze_schema_drift requires at least one ORM (Prisma/Drizzle/TypeORM).");
|
|
923
|
+
return {
|
|
924
|
+
drifts: [],
|
|
925
|
+
summary: { extra_in_orm: 0, extra_in_sql: 0, type_mismatches: 0, total: 0 },
|
|
926
|
+
orms_detected,
|
|
927
|
+
warnings,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
// Cross-reference: Prisma models vs SQL tables
|
|
931
|
+
const matchedSqlTables = new Set();
|
|
932
|
+
for (const model of prismaModels) {
|
|
933
|
+
const sqlTable = sqlTables.get(model.table.toLowerCase());
|
|
934
|
+
if (!sqlTable) {
|
|
935
|
+
// Prisma model has no matching SQL table → extra_in_orm
|
|
936
|
+
drifts.push({
|
|
937
|
+
kind: "extra_in_orm",
|
|
938
|
+
table: model.table,
|
|
939
|
+
orm: "prisma",
|
|
940
|
+
orm_file: model.file,
|
|
941
|
+
orm_line: model.line,
|
|
942
|
+
detail: `Prisma model "${model.name}" maps to table "${model.table}" which does not exist in SQL schema.`,
|
|
943
|
+
});
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
matchedSqlTables.add(model.table.toLowerCase());
|
|
947
|
+
// Compare fields
|
|
948
|
+
const sqlCols = sqlTable.columns;
|
|
949
|
+
const matchedSqlCols = new Set();
|
|
950
|
+
for (const field of model.fields) {
|
|
951
|
+
const sqlCol = sqlCols.get(field.db_name.toLowerCase());
|
|
952
|
+
if (!sqlCol) {
|
|
953
|
+
drifts.push({
|
|
954
|
+
kind: "extra_in_orm",
|
|
955
|
+
table: model.table,
|
|
956
|
+
column: field.name,
|
|
957
|
+
orm: "prisma",
|
|
958
|
+
orm_file: model.file,
|
|
959
|
+
orm_line: model.line,
|
|
960
|
+
orm_type: field.type,
|
|
961
|
+
detail: `Prisma field "${model.name}.${field.name}" (maps to "${field.db_name}") does not exist in SQL table "${model.table}".`,
|
|
962
|
+
});
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
matchedSqlCols.add(field.db_name.toLowerCase());
|
|
966
|
+
// Type compatibility check
|
|
967
|
+
const ormNorm = normalizeType(field.type);
|
|
968
|
+
const sqlNorm = normalizeType(sqlCol.type);
|
|
969
|
+
if (ormNorm !== sqlNorm && ormNorm !== "unknown" && sqlNorm !== "unknown") {
|
|
970
|
+
drifts.push({
|
|
971
|
+
kind: "type_mismatch",
|
|
972
|
+
table: model.table,
|
|
973
|
+
column: field.name,
|
|
974
|
+
orm: "prisma",
|
|
975
|
+
orm_file: model.file,
|
|
976
|
+
orm_line: model.line,
|
|
977
|
+
sql_file: sqlCol.file,
|
|
978
|
+
sql_line: sqlCol.line,
|
|
979
|
+
orm_type: field.type,
|
|
980
|
+
sql_type: sqlCol.type,
|
|
981
|
+
detail: `Type mismatch: Prisma "${model.name}.${field.name}" is "${field.type}" (${ormNorm}) but SQL "${model.table}.${field.db_name}" is "${sqlCol.type}" (${sqlNorm}).`,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// SQL columns not covered by any Prisma field → extra_in_sql (column-level)
|
|
986
|
+
for (const [colName, sqlCol] of sqlCols) {
|
|
987
|
+
if (matchedSqlCols.has(colName))
|
|
988
|
+
continue;
|
|
989
|
+
// Don't flag common auto-columns that Prisma often omits
|
|
990
|
+
if (/^(created_at|updated_at|deleted_at)$/i.test(colName))
|
|
991
|
+
continue;
|
|
992
|
+
drifts.push({
|
|
993
|
+
kind: "extra_in_sql",
|
|
994
|
+
table: model.table,
|
|
995
|
+
column: colName,
|
|
996
|
+
orm: "prisma",
|
|
997
|
+
sql_file: sqlCol.file,
|
|
998
|
+
sql_line: sqlCol.line,
|
|
999
|
+
sql_type: sqlCol.type,
|
|
1000
|
+
detail: `SQL column "${model.table}.${colName}" has no corresponding field in Prisma model "${model.name}".`,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// SQL tables with no Prisma model → extra_in_sql (table-level)
|
|
1005
|
+
for (const [tableName, sqlTable] of sqlTables) {
|
|
1006
|
+
if (matchedSqlTables.has(tableName))
|
|
1007
|
+
continue;
|
|
1008
|
+
drifts.push({
|
|
1009
|
+
kind: "extra_in_sql",
|
|
1010
|
+
table: tableName,
|
|
1011
|
+
orm: "prisma",
|
|
1012
|
+
sql_file: sqlTable.file,
|
|
1013
|
+
sql_line: sqlTable.line,
|
|
1014
|
+
detail: `SQL table "${tableName}" has no corresponding Prisma model.`,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
const summary = {
|
|
1018
|
+
extra_in_orm: drifts.filter((d) => d.kind === "extra_in_orm").length,
|
|
1019
|
+
extra_in_sql: drifts.filter((d) => d.kind === "extra_in_sql").length,
|
|
1020
|
+
type_mismatches: drifts.filter((d) => d.kind === "type_mismatch").length,
|
|
1021
|
+
total: drifts.length,
|
|
1022
|
+
};
|
|
1023
|
+
// Heuristic warning: if >50% of ORM tables have no SQL counterpart, this is
|
|
1024
|
+
// likely a Prisma-migrate style project where SQL files are incremental
|
|
1025
|
+
// migrations, not a full schema snapshot. In that case table-level drift
|
|
1026
|
+
// is noise — the real signal is field-level drift on matched tables.
|
|
1027
|
+
const totalOrmTables = prismaModels.length;
|
|
1028
|
+
const orphanOrmTables = drifts.filter((d) => d.kind === "extra_in_orm" && !d.column).length;
|
|
1029
|
+
if (totalOrmTables > 0 && orphanOrmTables / totalOrmTables > 0.5) {
|
|
1030
|
+
warnings.push(`${orphanOrmTables}/${totalOrmTables} Prisma models have no SQL counterpart. ` +
|
|
1031
|
+
`This likely means the project uses Prisma Migrate (incremental migrations) ` +
|
|
1032
|
+
`rather than a full schema.sql snapshot. Field-level drifts on matched tables ` +
|
|
1033
|
+
`are still meaningful; ignore table-level extra_in_orm drifts in this mode.`);
|
|
1034
|
+
}
|
|
1035
|
+
return { drifts, summary, orms_detected, warnings };
|
|
1036
|
+
}
|
|
1037
|
+
const DEFAULT_SQL_AUDIT_CHECKS = ["drift", "orphan", "lint", "dml", "complexity"];
|
|
1038
|
+
/**
|
|
1039
|
+
* Composite SQL audit — runs multiple diagnostic gates in a single call.
|
|
1040
|
+
* Mirrors framework_audit / nest_audit / audit_scan pattern.
|
|
1041
|
+
*
|
|
1042
|
+
* Individual gate functions (analyzeSchemaDrift, findOrphanTables, lintSchema,
|
|
1043
|
+
* scanDmlSafety, analyzeSchemaComplexity) remain exported for internal use
|
|
1044
|
+
* but are NOT registered as separate MCP tools. sql_audit is the single
|
|
1045
|
+
* discoverable entry point.
|
|
1046
|
+
*/
|
|
1047
|
+
export async function sqlAudit(repo, options) {
|
|
1048
|
+
const checks = options?.checks ?? DEFAULT_SQL_AUDIT_CHECKS;
|
|
1049
|
+
const filePattern = options?.file_pattern;
|
|
1050
|
+
const gates = [];
|
|
1051
|
+
const warnings = [];
|
|
1052
|
+
// Build option objects with only defined fields (for exactOptionalPropertyTypes compat)
|
|
1053
|
+
const scopedOpts = {};
|
|
1054
|
+
if (filePattern !== undefined)
|
|
1055
|
+
scopedOpts.file_pattern = filePattern;
|
|
1056
|
+
// Gate 1: schema_drift (ORM ↔ SQL drift)
|
|
1057
|
+
if (checks.includes("drift")) {
|
|
1058
|
+
const drift = await analyzeSchemaDrift(repo, scopedOpts);
|
|
1059
|
+
const criticalCount = drift.summary.type_mismatches;
|
|
1060
|
+
gates.push({
|
|
1061
|
+
check: "drift",
|
|
1062
|
+
pass: drift.summary.total === 0,
|
|
1063
|
+
critical: criticalCount > 0,
|
|
1064
|
+
finding_count: drift.summary.total,
|
|
1065
|
+
data: drift,
|
|
1066
|
+
summary: drift.summary.total === 0
|
|
1067
|
+
? "No schema drift detected"
|
|
1068
|
+
: `${drift.summary.total} drift${drift.summary.total === 1 ? "" : "s"}: ${drift.summary.extra_in_orm} extra in ORM, ${drift.summary.extra_in_sql} extra in SQL, ${drift.summary.type_mismatches} type mismatches`,
|
|
1069
|
+
});
|
|
1070
|
+
for (const w of drift.warnings)
|
|
1071
|
+
warnings.push(`drift: ${w}`);
|
|
1072
|
+
}
|
|
1073
|
+
// Gate 2: orphan_tables
|
|
1074
|
+
if (checks.includes("orphan")) {
|
|
1075
|
+
const orphan = await findOrphanTables(repo, scopedOpts);
|
|
1076
|
+
gates.push({
|
|
1077
|
+
check: "orphan",
|
|
1078
|
+
pass: orphan.orphan_count === 0,
|
|
1079
|
+
critical: false,
|
|
1080
|
+
finding_count: orphan.orphan_count,
|
|
1081
|
+
data: orphan,
|
|
1082
|
+
summary: orphan.orphan_count === 0
|
|
1083
|
+
? `No orphan tables (${orphan.total_tables} tables scanned)`
|
|
1084
|
+
: `${orphan.orphan_count}/${orphan.total_tables} tables with zero references`,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
// Gate 3: lint_schema
|
|
1088
|
+
if (checks.includes("lint")) {
|
|
1089
|
+
const lint = await lintSchema(repo, scopedOpts);
|
|
1090
|
+
gates.push({
|
|
1091
|
+
check: "lint",
|
|
1092
|
+
pass: lint.summary.total === 0,
|
|
1093
|
+
critical: false,
|
|
1094
|
+
finding_count: lint.summary.total,
|
|
1095
|
+
data: lint,
|
|
1096
|
+
summary: lint.summary.total === 0
|
|
1097
|
+
? "No schema lint violations"
|
|
1098
|
+
: `${lint.summary.total} lint violation${lint.summary.total === 1 ? "" : "s"}: ${Object.entries(lint.summary.by_rule).map(([r, n]) => `${r}=${n}`).join(", ")}`,
|
|
1099
|
+
});
|
|
1100
|
+
for (const w of lint.warnings)
|
|
1101
|
+
warnings.push(`lint: ${w}`);
|
|
1102
|
+
}
|
|
1103
|
+
// Gate 4: dml_safety
|
|
1104
|
+
if (checks.includes("dml")) {
|
|
1105
|
+
const dmlOpts = {};
|
|
1106
|
+
if (filePattern !== undefined)
|
|
1107
|
+
dmlOpts.file_pattern = filePattern;
|
|
1108
|
+
if (options?.max_results !== undefined)
|
|
1109
|
+
dmlOpts.max_results = options.max_results;
|
|
1110
|
+
const dml = await scanDmlSafety(repo, dmlOpts);
|
|
1111
|
+
const highSeverity = dml.findings.filter((f) => f.severity === "high").length;
|
|
1112
|
+
gates.push({
|
|
1113
|
+
check: "dml",
|
|
1114
|
+
pass: highSeverity === 0,
|
|
1115
|
+
critical: highSeverity > 0,
|
|
1116
|
+
finding_count: dml.summary.total,
|
|
1117
|
+
data: dml,
|
|
1118
|
+
summary: dml.summary.total === 0
|
|
1119
|
+
? `No DML safety issues (${dml.summary.files_scanned} files scanned)`
|
|
1120
|
+
: `${dml.summary.total} DML issue${dml.summary.total === 1 ? "" : "s"} (${highSeverity} high risk): ${Object.entries(dml.summary.by_rule).map(([r, n]) => `${r}=${n}`).join(", ")}`,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
// Gate 5: schema_complexity (god tables)
|
|
1124
|
+
if (checks.includes("complexity")) {
|
|
1125
|
+
const complexityOpts = { top_n: 10 };
|
|
1126
|
+
if (filePattern !== undefined)
|
|
1127
|
+
complexityOpts.file_pattern = filePattern;
|
|
1128
|
+
const complexity = await analyzeSchemaComplexity(repo, complexityOpts);
|
|
1129
|
+
// Threshold: score >= 25 = "needs refactor" (20 cols + 1 FK + 1 idx → 24.5)
|
|
1130
|
+
const godTables = complexity.tables.filter((t) => t.score >= 25);
|
|
1131
|
+
gates.push({
|
|
1132
|
+
check: "complexity",
|
|
1133
|
+
pass: godTables.length === 0,
|
|
1134
|
+
critical: false,
|
|
1135
|
+
finding_count: godTables.length,
|
|
1136
|
+
data: complexity,
|
|
1137
|
+
summary: godTables.length === 0
|
|
1138
|
+
? `No god tables detected (${complexity.tables.length} tables analyzed)`
|
|
1139
|
+
: `${godTables.length} god table${godTables.length === 1 ? "" : "s"}: ${godTables.slice(0, 3).map((t) => `${t.name}(${t.score.toFixed(0)})`).join(", ")}${godTables.length > 3 ? "..." : ""}`,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
const total_findings = gates.reduce((sum, g) => sum + g.finding_count, 0);
|
|
1143
|
+
const critical_findings = gates
|
|
1144
|
+
.filter((g) => g.critical)
|
|
1145
|
+
.reduce((sum, g) => sum + g.finding_count, 0);
|
|
1146
|
+
const gates_passed = gates.filter((g) => g.pass).length;
|
|
1147
|
+
const gates_failed = gates.filter((g) => !g.pass).length;
|
|
1148
|
+
return {
|
|
1149
|
+
gates,
|
|
1150
|
+
summary: {
|
|
1151
|
+
total_findings,
|
|
1152
|
+
critical_findings,
|
|
1153
|
+
gates_run: gates.length,
|
|
1154
|
+
gates_passed,
|
|
1155
|
+
gates_failed,
|
|
1156
|
+
},
|
|
1157
|
+
warnings,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
//# sourceMappingURL=sql-tools.js.map
|