scip-cli 2.7.0__tar.gz → 2.8.0__tar.gz

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.
Files changed (154) hide show
  1. {scip_cli-2.7.0 → scip_cli-2.8.0}/PKG-INFO +2 -2
  2. scip_cli-2.8.0/docs/port-v28-board.md +32 -0
  3. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/SKILL.md +3 -1
  4. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/__init__.py +1 -1
  5. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/__main__.py +9 -0
  6. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/reindex.py +8 -0
  7. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/config.py +6 -0
  8. scip_cli-2.8.0/scip_cli/exclude.py +108 -0
  9. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/convert.py +8 -2
  10. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/core.py +8 -1
  11. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/languages.py +6 -6
  12. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/orchestrate.py +3 -0
  13. scip_cli-2.8.0/scip_cli/indexing/postprocess.py +296 -0
  14. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/typescript.py +13 -3
  15. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/e2e_harness.py +3 -1
  16. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixture_catalog.py +8 -0
  17. scip_cli-2.8.0/tests/fixtures/typescript-project/src/__tests__/fixtureOnly.spec.ts +5 -0
  18. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_config.py +1 -0
  19. scip_cli-2.8.0/tests/test_exclude_globs.py +162 -0
  20. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_python_index_integration.py +1 -1
  21. scip_cli-2.7.0/scip_cli/indexing/postprocess.py +0 -152
  22. {scip_cli-2.7.0 → scip_cli-2.8.0}/.agents/commands/migration-problems.md +0 -0
  23. {scip_cli-2.7.0 → scip_cli-2.8.0}/.agents/rules/agent.mdc +0 -0
  24. {scip_cli-2.7.0 → scip_cli-2.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {scip_cli-2.7.0 → scip_cli-2.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {scip_cli-2.7.0 → scip_cli-2.8.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
  27. {scip_cli-2.7.0 → scip_cli-2.8.0}/.github/pull_request_template.md +0 -0
  28. {scip_cli-2.7.0 → scip_cli-2.8.0}/.github/workflows/ci.yml +0 -0
  29. {scip_cli-2.7.0 → scip_cli-2.8.0}/.gitignore +0 -0
  30. {scip_cli-2.7.0 → scip_cli-2.8.0}/.pre-commit-config.yaml +0 -0
  31. {scip_cli-2.7.0 → scip_cli-2.8.0}/AGENTS.md +0 -0
  32. {scip_cli-2.7.0 → scip_cli-2.8.0}/CODE_OF_CONDUCT.md +0 -0
  33. {scip_cli-2.7.0 → scip_cli-2.8.0}/CONTRIBUTING.md +0 -0
  34. {scip_cli-2.7.0 → scip_cli-2.8.0}/LICENSE +0 -0
  35. {scip_cli-2.7.0 → scip_cli-2.8.0}/README.md +0 -0
  36. {scip_cli-2.7.0 → scip_cli-2.8.0}/SECURITY.md +0 -0
  37. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/port.md +0 -0
  38. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/conclusions.md +0 -0
  39. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/experiment-summary.md +0 -0
  40. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/go-experiment.md +0 -0
  41. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/go.md +0 -0
  42. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/plan.md +0 -0
  43. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/rust-experiment.md +0 -0
  44. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/rust.md +0 -0
  45. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/summary.md +0 -0
  46. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/zig-experiment.md +0 -0
  47. {scip_cli-2.7.0 → scip_cli-2.8.0}/docs/ports/zig.md +0 -0
  48. {scip_cli-2.7.0 → scip_cli-2.8.0}/pyproject.toml +0 -0
  49. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/__init__.py +0 -0
  50. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/common.py +0 -0
  51. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/file.py +0 -0
  52. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/graph.py +0 -0
  53. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/live.py +0 -0
  54. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/project.py +0 -0
  55. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/sections.py +0 -0
  56. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/symbol.py +0 -0
  57. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/analyze/targets.py +0 -0
  58. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/cache.py +0 -0
  59. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/cli_args.py +0 -0
  60. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/__init__.py +0 -0
  61. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/analyze.py +0 -0
  62. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/code.py +0 -0
  63. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/deps.py +0 -0
  64. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/members.py +0 -0
  65. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/rdeps.py +0 -0
  66. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/refs.py +0 -0
  67. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/search.py +0 -0
  68. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/skill.py +0 -0
  69. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/commands/symbols.py +0 -0
  70. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/debug.py +0 -0
  71. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/discover.py +0 -0
  72. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/__init__.py +0 -0
  73. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/constants.py +0 -0
  74. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/indexing/runners.py +0 -0
  75. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/merge.py +0 -0
  76. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/output.py +0 -0
  77. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/paths.py +0 -0
  78. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/project.py +0 -0
  79. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/queries.py +0 -0
  80. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/scip_tool.py +0 -0
  81. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/scope.py +0 -0
  82. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/session.py +0 -0
  83. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/source.py +0 -0
  84. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/sql.py +0 -0
  85. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/symbols.py +0 -0
  86. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/targets.py +0 -0
  87. {scip_cli-2.7.0 → scip_cli-2.8.0}/scip_cli/tsconfig.py +0 -0
  88. {scip_cli-2.7.0 → scip_cli-2.8.0}/scripts/bench.sh +0 -0
  89. {scip_cli-2.7.0 → scip_cli-2.8.0}/scripts/bench_postprocess.py +0 -0
  90. {scip_cli-2.7.0 → scip_cli-2.8.0}/scripts/build.sh +0 -0
  91. {scip_cli-2.7.0 → scip_cli-2.8.0}/scripts/publish.sh +0 -0
  92. {scip_cli-2.7.0 → scip_cli-2.8.0}/scripts/test.sh +0 -0
  93. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/__init__.py +0 -0
  94. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/analyze_db.py +0 -0
  95. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/bench_db.py +0 -0
  96. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/conftest.py +0 -0
  97. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/package.json +0 -0
  98. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/app/handler.ts +0 -0
  99. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/config.ts +0 -0
  100. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/consumer.ts +0 -0
  101. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/dead.ts +0 -0
  102. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/domain/i18n/en.ts +0 -0
  103. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/domain/i18n/index.ts +0 -0
  104. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/domain/i18n/useLocale.ts +0 -0
  105. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/domain/labels/index.ts +0 -0
  106. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/events/streamTypes.ts +0 -0
  107. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/gateway/index.ts +0 -0
  108. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/helper.ts +0 -0
  109. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/hooks/useHookA.ts +0 -0
  110. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/hooks/useHookB.ts +0 -0
  111. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/hooks/useItems.ts +0 -0
  112. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/index.ts +0 -0
  113. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/integrations/inferenceClient.ts +0 -0
  114. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/lib/handler.ts +0 -0
  115. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/loaders/useInference.ts +0 -0
  116. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/models/MutationAggregate.ts +0 -0
  117. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/pages/panelPage.ts +0 -0
  118. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/rules/applyRule.ts +0 -0
  119. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/types/a.ts +0 -0
  120. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/types/b.ts +0 -0
  121. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/ui/Button.ts +0 -0
  122. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/ui/LazyPanel.ts +0 -0
  123. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/ui/buttonConsumer.ts +0 -0
  124. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/ui/menuModule.ts +0 -0
  125. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/user.ts +0 -0
  126. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/widget.ts +0 -0
  127. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/src/widgets/OrphanWidget.ts +0 -0
  128. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/fixtures/typescript-project/tsconfig.json +0 -0
  129. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/perf_util.py +0 -0
  130. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_analyze.py +0 -0
  131. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_analyze_graph.py +0 -0
  132. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_analyze_perf.py +0 -0
  133. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_bench_queries.py +0 -0
  134. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_cache.py +0 -0
  135. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_composability.py +0 -0
  136. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_discover.py +0 -0
  137. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_e2e.py +0 -0
  138. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_e2e_analyze_patterns.py +0 -0
  139. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_e2e_perf.py +0 -0
  140. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_index_batching.py +0 -0
  141. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_index_prune.py +0 -0
  142. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_indexer_env.py +0 -0
  143. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_indexer_fallback.py +0 -0
  144. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_merge.py +0 -0
  145. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_multi_symbol.py +0 -0
  146. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_pure_functions.py +0 -0
  147. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_qualified_symbols.py +0 -0
  148. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_reindex.py +0 -0
  149. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_scip_tool.py +0 -0
  150. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_scope.py +0 -0
  151. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_symbols_freq.py +0 -0
  152. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_targets.py +0 -0
  153. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_tsconfig.py +0 -0
  154. {scip_cli-2.7.0 → scip_cli-2.8.0}/tests/test_typescript_projects.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: scip-cli
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: Fast code intelligence via SCIP indexes
5
5
  Project-URL: Homepage, https://github.com/flesler/scip-cli
6
6
  Author: Ariel Flesler
@@ -0,0 +1,32 @@
1
+ # v2.8.0 multi-port board
2
+
3
+ **Goal:** Ship Python v2.8.0 (exclude globs), then port full v2.6.0→v2.8.0 gap to go/rust/zig in `/tmp`, direct-to-main, smoke-tested. Mirror GitHub releases optional.
4
+
5
+ **Frozen reference:** `/tmp/scip-cli-2.8.0-freeze.patch` (pre-commit snapshot)
6
+
7
+ ## Gap to port (per mirror)
8
+
9
+ | Release | Features | Manual port |
10
+ |---|---|---|
11
+ | v2.7.0 | `analyze --check`, `dead_files`, section FP warnings | analyze/*, commands/analyze, tests |
12
+ | v2.8.0 | `reindex --exclude`, `excludeGlobs`, post-process prune | exclude, indexing, config, reindex, tests |
13
+
14
+ `scripts/sync-upstream.sh --ref v2.8.0` covers SKILL, fixtures, templates only.
15
+
16
+ ## Lanes
17
+
18
+ | Lane | Path | Status | Notes |
19
+ |---|---|---|---|
20
+ | P0 Python | `~/Code/scip-cli` | in progress | commit → v2.8.0 → push → release |
21
+ | G1-go | `/tmp/scip-cli-go` | pending | direct-to-main |
22
+ | G1-rust | `/tmp/scip-cli-rust` | pending | direct-to-main |
23
+ | G1-zig | `/tmp/scip-cli-zig` | pending | direct-to-main |
24
+
25
+ ## Verify log
26
+
27
+ | Check | Python | Go | Rust | Zig |
28
+ |---|---|---|---|---|
29
+ | scripts/test.sh | | | | |
30
+ | exclude dogfood | | | | |
31
+ | analyze --check | n/a | | | |
32
+ | readonly /iterate review | | | | |
@@ -160,7 +160,9 @@ Directory detection uses the filesystem when present, otherwise an indexed path
160
160
  ### reindex
161
161
 
162
162
  ```bash
163
- reindex [--path DIR ...] [--tsconfig FILE_OR_GLOB ...] [--with-external]
163
+ reindex [--path DIR ...] [--tsconfig FILE_OR_GLOB ...] [--exclude GLOB ...] [--with-external]
164
164
  ```
165
165
 
166
166
  `--path` and `--tsconfig` cannot be combined (**TypeScript only**). `--tsconfig` takes `tsconfig*.json` files (repeatable; globs expanded inside the tool). File-based runs default to one `scip-typescript` process per file so each gets its own heap (`SCIP_CLI_TS_INDEX_BATCH_SIZE` still overrides). Scope is saved as `index-scope.json` and reused until a full `reindex` with neither flag.
167
+
168
+ `--exclude GLOB` omits matching files from the SQLite index after conversion (repeatable; merged with `excludeGlobs` in `.scip-cli.json`). Persisted in `index-exclude.json` until a full `reindex` with no `--exclude`. Patterns without `/` match basenames (`*.test.ts`); patterns with `/` match repo-relative paths (`tests/**`, `**/__tests__/**`). Indexers still parse excluded files when production code imports them — post-process removal is authoritative.
@@ -1,3 +1,3 @@
1
1
  """scip-cli: Fast code intelligence via SCIP indexes."""
2
2
 
3
- __version__ = "2.7.0"
3
+ __version__ = "2.8.0"
@@ -166,6 +166,15 @@ def main() -> None:
166
166
  "Cannot be combined with --path."
167
167
  ),
168
168
  )
169
+ reindex_parser.add_argument(
170
+ "--exclude",
171
+ action="append",
172
+ metavar="GLOB",
173
+ help=(
174
+ "Omit indexed files matching GLOB from the SQLite index "
175
+ "(repeatable; merged with excludeGlobs in .scip-cli.json; persisted until full reindex)"
176
+ ),
177
+ )
169
178
  reindex_parser.add_argument(
170
179
  "--with-external",
171
180
  action="store_true",
@@ -9,6 +9,7 @@ from ..cache import (
9
9
  index_db_path,
10
10
  promote_next_index,
11
11
  )
12
+ from ..exclude import save_persisted_exclude_globs
12
13
  from ..indexing import index_project, log_index_complete
13
14
  from ..paths import normalize_path_scope
14
15
  from ..project import Language, find_project_root_and_language
@@ -24,6 +25,7 @@ def main(args):
24
25
 
25
26
  path_args = getattr(args, "path", None) or []
26
27
  tsconfig_args = getattr(args, "tsconfig", None) or []
28
+ exclude_args = getattr(args, "exclude", None) or []
27
29
  if path_args and tsconfig_args:
28
30
  print("Error: reindex --path and --tsconfig cannot be combined", file=sys.stderr)
29
31
  sys.exit(1)
@@ -68,6 +70,12 @@ def main(args):
68
70
  else:
69
71
  save_index_scope(root, None)
70
72
 
73
+ if exclude_args:
74
+ save_persisted_exclude_globs(root, exclude_args)
75
+ print(f"Index exclude: {', '.join(exclude_args)}", file=sys.stderr)
76
+ else:
77
+ save_persisted_exclude_globs(root, None)
78
+
71
79
  cache_dir = get_cache_dir(root)
72
80
  cache_dir.mkdir(parents=True, exist_ok=True)
73
81
 
@@ -16,6 +16,7 @@ class ProjectSettings:
16
16
  max_heap_mb: int | None = None
17
17
  index_roots: list[str] = field(default_factory=list)
18
18
  only_index_roots: bool = False
19
+ exclude_globs: list[str] = field(default_factory=list)
19
20
 
20
21
 
21
22
  def load_project_config(project_root: Path) -> ProjectSettings:
@@ -44,10 +45,15 @@ def load_project_config(project_root: Path) -> ProjectSettings:
44
45
  if type(only_index_roots) is not bool:
45
46
  raise RuntimeError(f"Invalid {CONFIG_FILENAME}: onlyIndexRoots must be a boolean")
46
47
 
48
+ exclude_globs = data.get("excludeGlobs", [])
49
+ if not isinstance(exclude_globs, list) or not all(isinstance(p, str) for p in exclude_globs):
50
+ raise RuntimeError(f"Invalid {CONFIG_FILENAME}: excludeGlobs must be a string array")
51
+
47
52
  return ProjectSettings(
48
53
  max_heap_mb=max_heap_mb,
49
54
  index_roots=index_roots,
50
55
  only_index_roots=only_index_roots,
56
+ exclude_globs=exclude_globs,
51
57
  )
52
58
 
53
59
 
@@ -0,0 +1,108 @@
1
+ """Persisted index exclude globs and path matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path, PurePosixPath
8
+
9
+ from .config import load_project_config
10
+
11
+ EXCLUDE_FILENAME = "index-exclude.json"
12
+
13
+
14
+ def _exclude_path(project_root: Path) -> Path:
15
+ from .cache import get_cache_dir
16
+
17
+ return get_cache_dir(project_root) / EXCLUDE_FILENAME
18
+
19
+
20
+ def load_persisted_exclude_globs(project_root: Path) -> tuple[str, ...]:
21
+ """Load exclude globs from the last scoped reindex --exclude, if any."""
22
+ path = _exclude_path(project_root)
23
+ if not path.is_file():
24
+ return ()
25
+ try:
26
+ data = json.loads(path.read_text(encoding="utf-8"))
27
+ except (OSError, json.JSONDecodeError):
28
+ return ()
29
+ if not isinstance(data, dict):
30
+ return ()
31
+ raw = data.get("globs")
32
+ if not raw or not isinstance(raw, list) or not all(isinstance(g, str) for g in raw):
33
+ return ()
34
+ return tuple(raw)
35
+
36
+
37
+ def save_persisted_exclude_globs(project_root: Path, globs: list[str] | None) -> None:
38
+ """Persist or clear reindex --exclude globs for a project."""
39
+ path = _exclude_path(project_root)
40
+ if not globs:
41
+ if path.is_file():
42
+ path.unlink()
43
+ return
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ path.write_text(json.dumps({"globs": globs}, indent=2) + "\n", encoding="utf-8")
46
+
47
+
48
+ def resolve_exclude_globs(project_root: Path) -> tuple[str, ...]:
49
+ """Merge .scip-cli.json excludeGlobs with persisted reindex --exclude globs."""
50
+ settings = load_project_config(project_root)
51
+ merged: list[str] = []
52
+ seen: set[str] = set()
53
+ for glob in (*settings.exclude_globs, *load_persisted_exclude_globs(project_root)):
54
+ if glob and glob not in seen:
55
+ seen.add(glob)
56
+ merged.append(glob)
57
+ return tuple(merged)
58
+
59
+
60
+ def _glob_to_regex(pattern: str) -> re.Pattern[str]:
61
+ """Translate a repo-relative glob to a regex (supports **, *, ?)."""
62
+ normalized = pattern.replace("\\", "/").lstrip("/")
63
+ parts: list[str] = []
64
+ index = 0
65
+ while index < len(normalized):
66
+ char = normalized[index]
67
+ if char == "*":
68
+ if index + 1 < len(normalized) and normalized[index + 1] == "*":
69
+ if index + 2 < len(normalized) and normalized[index + 2] == "/":
70
+ parts.append("(?:.+/)?")
71
+ index += 3
72
+ continue
73
+ parts.append(".*")
74
+ index += 2
75
+ continue
76
+ parts.append("[^/]*")
77
+ index += 1
78
+ continue
79
+ if char == "?":
80
+ parts.append("[^/]")
81
+ index += 1
82
+ continue
83
+ if char in ".^$+{}|()[]":
84
+ parts.append(re.escape(char))
85
+ else:
86
+ parts.append(char)
87
+ index += 1
88
+ return re.compile("^" + "".join(parts) + "$")
89
+
90
+
91
+ def path_matches_glob(relative_path: str, pattern: str) -> bool:
92
+ """Return True when a repo-relative path matches an exclude glob.
93
+
94
+ Patterns without '/' match the basename only (e.g. ``*.test.ts``).
95
+ Patterns with '/' match the full posix path (``tests/**``, ``src/*.spec.ts``).
96
+ """
97
+ path = relative_path.replace("\\", "/").lstrip("/")
98
+ glob = pattern.replace("\\", "/").lstrip("/")
99
+ if not glob:
100
+ return False
101
+ target = PurePosixPath(path).name if "/" not in glob else path
102
+ return bool(_glob_to_regex(glob).match(target))
103
+
104
+
105
+ def path_matches_any_glob(relative_path: str, patterns: tuple[str, ...] | list[str]) -> bool:
106
+ if not patterns:
107
+ return False
108
+ return any(path_matches_glob(relative_path, pattern) for pattern in patterns)
@@ -68,7 +68,13 @@ def prefix_document_paths(db_path: Path, prefix: str) -> None:
68
68
  conn.close()
69
69
 
70
70
 
71
- def convert_scip_to_db(scip_path, db_path, *, document_path_prefix: Path | str | None = None):
71
+ def convert_scip_to_db(
72
+ scip_path,
73
+ db_path,
74
+ *,
75
+ document_path_prefix: Path | str | None = None,
76
+ exclude_globs: tuple[str, ...] | None = None,
77
+ ):
72
78
  """Convert a SCIP protobuf file to a SQLite index at db_path."""
73
79
  db_path = Path(db_path)
74
80
  db_path.parent.mkdir(parents=True, exist_ok=True)
@@ -88,7 +94,7 @@ def convert_scip_to_db(scip_path, db_path, *, document_path_prefix: Path | str |
88
94
  if not db_path.exists():
89
95
  raise RuntimeError("Failed to convert index")
90
96
 
91
- postprocess_index(db_path)
97
+ postprocess_index(db_path, exclude_globs=exclude_globs or ())
92
98
  prefix = project_path_prefix(document_path_prefix)
93
99
  if prefix is not None:
94
100
  prefix_document_paths(db_path, prefix)
@@ -17,6 +17,7 @@ from ..cache import (
17
17
  )
18
18
  from ..config import load_project_config
19
19
  from ..discover import discover_golang_modules, discover_python_projects, discover_rust_crates
20
+ from ..exclude import resolve_exclude_globs
20
21
  from .constants import DEFAULT_MAX_HEAP_MB
21
22
  from .languages import index_golang_module, index_python_project, index_rust_crate
22
23
  from .orchestrate import index_discovered_projects
@@ -87,10 +88,13 @@ def index_project(root, lang, cache_dir, *, replace=False, log=True):
87
88
  cache_dir.mkdir(parents=True, exist_ok=True)
88
89
  root = Path(root).resolve()
89
90
  env = indexer_env(root)
91
+ exclude_globs = resolve_exclude_globs(root)
90
92
 
91
93
  if lang == Language.TYPESCRIPT:
92
94
  projects = typescript_projects(root)
93
- output_db, _indexed, skipped, total = index_typescript(root, cache_dir, projects, env, replace=replace)
95
+ output_db, _indexed, skipped, total = index_typescript(
96
+ root, cache_dir, projects, env, replace=replace, exclude_globs=exclude_globs
97
+ )
94
98
  if log:
95
99
  log_index_complete(
96
100
  output_db,
@@ -110,6 +114,7 @@ def index_project(root, lang, cache_dir, *, replace=False, log=True):
110
114
  replace=replace,
111
115
  progress_noun="Python packages",
112
116
  index_one=index_python_project,
117
+ exclude_globs=exclude_globs,
113
118
  )
114
119
  if log:
115
120
  log_index_complete(
@@ -130,6 +135,7 @@ def index_project(root, lang, cache_dir, *, replace=False, log=True):
130
135
  replace=replace,
131
136
  progress_noun="Go modules",
132
137
  index_one=index_golang_module,
138
+ exclude_globs=exclude_globs,
133
139
  )
134
140
  if log:
135
141
  log_index_complete(
@@ -150,6 +156,7 @@ def index_project(root, lang, cache_dir, *, replace=False, log=True):
150
156
  replace=replace,
151
157
  progress_noun="Rust crates",
152
158
  index_one=index_rust_crate,
159
+ exclude_globs=exclude_globs,
153
160
  )
154
161
  if log:
155
162
  log_index_complete(
@@ -13,7 +13,7 @@ def _project_cwd(root: Path, project: Path) -> Path:
13
13
  return root if project == Path(".") else root / project
14
14
 
15
15
 
16
- def index_python_project(root, project, work_dir, env, *, output_db=None):
16
+ def index_python_project(root, project, work_dir, env, *, output_db=None, exclude_globs=()):
17
17
  """Index one Python package directory into work_dir/index.db (or output_db when set)."""
18
18
  work_dir = Path(work_dir)
19
19
  work_dir.mkdir(parents=True, exist_ok=True)
@@ -31,13 +31,13 @@ def index_python_project(root, project, work_dir, env, *, output_db=None):
31
31
  if result.returncode != 0:
32
32
  return label, None, result.stderr.strip() or "indexing failed"
33
33
  try:
34
- convert_scip_to_db(part_scip, db_path, document_path_prefix=project)
34
+ convert_scip_to_db(part_scip, db_path, document_path_prefix=project, exclude_globs=exclude_globs)
35
35
  finally:
36
36
  part_scip.unlink(missing_ok=True)
37
37
  return label, db_path, None
38
38
 
39
39
 
40
- def index_golang_module(root, module, work_dir, env, *, output_db=None):
40
+ def index_golang_module(root, module, work_dir, env, *, output_db=None, exclude_globs=()):
41
41
  """Index one Go module directory into work_dir/index.db (or output_db when set)."""
42
42
  work_dir = Path(work_dir)
43
43
  work_dir.mkdir(parents=True, exist_ok=True)
@@ -55,13 +55,13 @@ def index_golang_module(root, module, work_dir, env, *, output_db=None):
55
55
  if result.returncode != 0:
56
56
  return label, None, result.stderr.strip() or "indexing failed"
57
57
  try:
58
- convert_scip_to_db(part_scip, db_path, document_path_prefix=module)
58
+ convert_scip_to_db(part_scip, db_path, document_path_prefix=module, exclude_globs=exclude_globs)
59
59
  finally:
60
60
  part_scip.unlink(missing_ok=True)
61
61
  return label, db_path, None
62
62
 
63
63
 
64
- def index_rust_crate(root, crate, work_dir, env, *, output_db=None):
64
+ def index_rust_crate(root, crate, work_dir, env, *, output_db=None, exclude_globs=()):
65
65
  """Index one Rust crate directory into work_dir/index.db (or output_db when set)."""
66
66
  work_dir = Path(work_dir)
67
67
  work_dir.mkdir(parents=True, exist_ok=True)
@@ -79,7 +79,7 @@ def index_rust_crate(root, crate, work_dir, env, *, output_db=None):
79
79
  if result.returncode != 0:
80
80
  return label, None, result.stderr.strip() or "indexing failed"
81
81
  try:
82
- convert_scip_to_db(part_scip, db_path, document_path_prefix=crate)
82
+ convert_scip_to_db(part_scip, db_path, document_path_prefix=crate, exclude_globs=exclude_globs)
83
83
  finally:
84
84
  part_scip.unlink(missing_ok=True)
85
85
  return label, db_path, None
@@ -103,6 +103,7 @@ def index_discovered_projects(
103
103
  replace: bool,
104
104
  progress_noun: str,
105
105
  index_one,
106
+ exclude_globs: tuple[str, ...] = (),
106
107
  ) -> tuple[Path, int, int, int]:
107
108
  """Index one SCIP unit per project path; merge when multiple part DBs."""
108
109
  output_db = index_db_path(cache_dir, replace=replace)
@@ -132,6 +133,7 @@ def index_discovered_projects(
132
133
  project,
133
134
  tmpdir_path / f"part-{index}",
134
135
  env,
136
+ exclude_globs=exclude_globs,
135
137
  ): (index, project)
136
138
  for index, project in enumerate(projects, start=1)
137
139
  }
@@ -160,6 +162,7 @@ def index_discovered_projects(
160
162
  cache_dir if direct_output else tmpdir_path / f"part-{index}",
161
163
  env,
162
164
  output_db=direct_output,
165
+ exclude_globs=exclude_globs,
163
166
  )
164
167
  if db_path is None:
165
168
  skipped += 1
@@ -0,0 +1,296 @@
1
+ """Shrink converted SQLite indexes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sqlite3
7
+
8
+
9
+ def _replace_table(conn, old_name: str, new_name: str) -> None:
10
+ conn.execute(f"DROP TABLE {old_name}")
11
+ conn.execute(f"ALTER TABLE {new_name} RENAME TO {old_name}")
12
+
13
+
14
+ def _trim_unused_columns(conn, keep_external=False):
15
+ """Remove columns we never use; omit variable symbols while rebuilding global_symbols.
16
+
17
+ Args:
18
+ conn: SQLite connection
19
+ keep_external: If True, keep symbols without definitions (external libs)
20
+ """
21
+ conn.execute("""
22
+ CREATE TABLE documents_new (
23
+ id INTEGER PRIMARY KEY,
24
+ relative_path TEXT NOT NULL UNIQUE
25
+ )
26
+ """)
27
+ conn.execute("INSERT INTO documents_new (id, relative_path) SELECT id, relative_path FROM documents")
28
+ _replace_table(conn, "documents", "documents_new")
29
+
30
+ from ..symbols import sql_exclude_variable_symbols
31
+
32
+ exclude = sql_exclude_variable_symbols("symbol")
33
+
34
+ # Build the WHERE clause: exclude variables AND optionally require definitions
35
+ where_clauses = [exclude]
36
+ if not keep_external:
37
+ # Keep symbols that are either:
38
+ # 1. Defined in the project (have defn_enclosing_ranges entries)
39
+ # 2. Structural symbols needed for type analysis (type literals, parameters)
40
+ # 3. Functions/methods (even without defs, they may be project code)
41
+ #
42
+ # SCIP symbol format patterns (language-agnostic):
43
+ # - %typeLiteral% : Type/interface fields (e.g., Options#typeLiteral0:verbose.)
44
+ # - %).(% : Function parameters (e.g., greet().(name))
45
+ # - %().% : Functions and methods (e.g., Widget.run()., greet().)
46
+ where_clauses.append(
47
+ "EXISTS (SELECT 1 FROM defn_enclosing_ranges der WHERE der.symbol_id = global_symbols.id) "
48
+ + "OR symbol LIKE '%typeLiteral%' "
49
+ + "OR symbol LIKE '%).(%' "
50
+ + "OR symbol LIKE '%().%'"
51
+ )
52
+
53
+ where_clause = " AND ".join(where_clauses)
54
+
55
+ conn.execute("""
56
+ CREATE TABLE global_symbols_new (
57
+ id INTEGER PRIMARY KEY,
58
+ symbol TEXT NOT NULL UNIQUE,
59
+ display_name TEXT,
60
+ kind INTEGER
61
+ )
62
+ """)
63
+ conn.execute(f"""
64
+ INSERT INTO global_symbols_new (id, symbol, display_name, kind)
65
+ SELECT id, symbol, display_name, kind FROM global_symbols
66
+ WHERE {where_clause}
67
+ """)
68
+ _replace_table(conn, "global_symbols", "global_symbols_new")
69
+
70
+
71
+ def _trim_mentions_to_known_symbols(conn):
72
+ """Copy mentions that reference retained symbols (drops variable refs without DELETE)."""
73
+ if not conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='mentions' LIMIT 1").fetchone():
74
+ return
75
+ conn.execute("""
76
+ CREATE TABLE mentions_new (
77
+ chunk_id INTEGER NOT NULL,
78
+ symbol_id INTEGER NOT NULL,
79
+ role INTEGER NOT NULL,
80
+ PRIMARY KEY (chunk_id, symbol_id, role)
81
+ )
82
+ """)
83
+ conn.execute("""
84
+ INSERT INTO mentions_new (chunk_id, symbol_id, role)
85
+ SELECT m.chunk_id, m.symbol_id, m.role
86
+ FROM mentions m
87
+ JOIN global_symbols g ON g.id = m.symbol_id
88
+ """)
89
+ _replace_table(conn, "mentions", "mentions_new")
90
+
91
+
92
+ def _trim_defn_to_known_symbols(conn):
93
+ """Copy defn rows for retained symbols only."""
94
+ if not conn.execute(
95
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='defn_enclosing_ranges' LIMIT 1"
96
+ ).fetchone():
97
+ return
98
+ conn.execute("""
99
+ CREATE TABLE defn_enclosing_ranges_new (
100
+ id INTEGER PRIMARY KEY,
101
+ document_id INTEGER NOT NULL,
102
+ symbol_id INTEGER NOT NULL,
103
+ start_line INTEGER NOT NULL,
104
+ start_char INTEGER NOT NULL,
105
+ end_line INTEGER NOT NULL,
106
+ end_char INTEGER NOT NULL
107
+ )
108
+ """)
109
+ conn.execute("""
110
+ INSERT INTO defn_enclosing_ranges_new (
111
+ id, document_id, symbol_id, start_line, start_char, end_line, end_char
112
+ )
113
+ SELECT d.id, d.document_id, d.symbol_id, d.start_line, d.start_char, d.end_line, d.end_char
114
+ FROM defn_enclosing_ranges d
115
+ JOIN global_symbols g ON g.id = d.symbol_id
116
+ """)
117
+ _replace_table(conn, "defn_enclosing_ranges", "defn_enclosing_ranges_new")
118
+
119
+
120
+ def _prune_excluded_documents(conn: sqlite3.Connection, exclude_globs: tuple[str, ...]) -> None:
121
+ """Drop documents matching exclude globs and cascade to chunks, mentions, defs, symbols."""
122
+ from ..exclude import path_matches_any_glob
123
+
124
+ if not exclude_globs:
125
+ return
126
+
127
+ rows = conn.execute("SELECT id, relative_path FROM documents").fetchall()
128
+ exclude_ids = {row[0] for row in rows if path_matches_any_glob(row[1], exclude_globs)}
129
+ if not exclude_ids:
130
+ return
131
+
132
+ conn.execute("""
133
+ CREATE TABLE documents_new (
134
+ id INTEGER PRIMARY KEY,
135
+ relative_path TEXT NOT NULL UNIQUE
136
+ )
137
+ """)
138
+ conn.execute(
139
+ "INSERT INTO documents_new (id, relative_path) SELECT id, relative_path FROM documents WHERE id NOT IN "
140
+ + f"({','.join('?' * len(exclude_ids))})",
141
+ tuple(exclude_ids),
142
+ )
143
+ _replace_table(conn, "documents", "documents_new")
144
+
145
+ if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='chunks' LIMIT 1").fetchone():
146
+ conn.execute("""
147
+ CREATE TABLE chunks_new (
148
+ id INTEGER PRIMARY KEY,
149
+ document_id INTEGER NOT NULL,
150
+ chunk_index INTEGER NOT NULL,
151
+ start_line INTEGER NOT NULL,
152
+ end_line INTEGER NOT NULL,
153
+ occurrences BLOB NOT NULL
154
+ )
155
+ """)
156
+ conn.execute(
157
+ "INSERT INTO chunks_new SELECT c.* FROM chunks c JOIN documents d ON c.document_id = d.id",
158
+ )
159
+ _replace_table(conn, "chunks", "chunks_new")
160
+
161
+ if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='mentions' LIMIT 1").fetchone():
162
+ conn.execute("""
163
+ CREATE TABLE mentions_new (
164
+ chunk_id INTEGER NOT NULL,
165
+ symbol_id INTEGER NOT NULL,
166
+ role INTEGER NOT NULL,
167
+ PRIMARY KEY (chunk_id, symbol_id, role)
168
+ )
169
+ """)
170
+ conn.execute("""
171
+ INSERT INTO mentions_new (chunk_id, symbol_id, role)
172
+ SELECT m.chunk_id, m.symbol_id, m.role
173
+ FROM mentions m
174
+ JOIN chunks c ON c.id = m.chunk_id
175
+ """)
176
+ _replace_table(conn, "mentions", "mentions_new")
177
+
178
+ if conn.execute(
179
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='defn_enclosing_ranges' LIMIT 1"
180
+ ).fetchone():
181
+ conn.execute("""
182
+ CREATE TABLE defn_enclosing_ranges_new (
183
+ id INTEGER PRIMARY KEY,
184
+ document_id INTEGER NOT NULL,
185
+ symbol_id INTEGER NOT NULL,
186
+ start_line INTEGER NOT NULL,
187
+ start_char INTEGER NOT NULL,
188
+ end_line INTEGER NOT NULL,
189
+ end_char INTEGER NOT NULL
190
+ )
191
+ """)
192
+ conn.execute("""
193
+ INSERT INTO defn_enclosing_ranges_new (
194
+ id, document_id, symbol_id, start_line, start_char, end_line, end_char
195
+ )
196
+ SELECT d.id, d.document_id, d.symbol_id, d.start_line, d.start_char, d.end_line, d.end_char
197
+ FROM defn_enclosing_ranges d
198
+ JOIN documents doc ON doc.id = d.document_id
199
+ """)
200
+ _replace_table(conn, "defn_enclosing_ranges", "defn_enclosing_ranges_new")
201
+
202
+ conn.execute("""
203
+ CREATE TABLE global_symbols_new (
204
+ id INTEGER PRIMARY KEY,
205
+ symbol TEXT NOT NULL UNIQUE,
206
+ display_name TEXT,
207
+ kind INTEGER
208
+ )
209
+ """)
210
+ conn.execute("""
211
+ INSERT INTO global_symbols_new (id, symbol, display_name, kind)
212
+ SELECT g.id, g.symbol, g.display_name, g.kind
213
+ FROM global_symbols g
214
+ WHERE EXISTS (SELECT 1 FROM defn_enclosing_ranges d WHERE d.symbol_id = g.id)
215
+ OR EXISTS (SELECT 1 FROM mentions m WHERE m.symbol_id = g.id)
216
+ """)
217
+ _replace_table(conn, "global_symbols", "global_symbols_new")
218
+
219
+ if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='mentions' LIMIT 1").fetchone():
220
+ conn.execute("""
221
+ CREATE TABLE mentions_new (
222
+ chunk_id INTEGER NOT NULL,
223
+ symbol_id INTEGER NOT NULL,
224
+ role INTEGER NOT NULL,
225
+ PRIMARY KEY (chunk_id, symbol_id, role)
226
+ )
227
+ """)
228
+ conn.execute("""
229
+ INSERT INTO mentions_new (chunk_id, symbol_id, role)
230
+ SELECT m.chunk_id, m.symbol_id, m.role
231
+ FROM mentions m
232
+ JOIN global_symbols g ON g.id = m.symbol_id
233
+ """)
234
+ _replace_table(conn, "mentions", "mentions_new")
235
+
236
+ if conn.execute(
237
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='defn_enclosing_ranges' LIMIT 1"
238
+ ).fetchone():
239
+ conn.execute("""
240
+ CREATE TABLE defn_enclosing_ranges_new (
241
+ id INTEGER PRIMARY KEY,
242
+ document_id INTEGER NOT NULL,
243
+ symbol_id INTEGER NOT NULL,
244
+ start_line INTEGER NOT NULL,
245
+ start_char INTEGER NOT NULL,
246
+ end_line INTEGER NOT NULL,
247
+ end_char INTEGER NOT NULL
248
+ )
249
+ """)
250
+ conn.execute("""
251
+ INSERT INTO defn_enclosing_ranges_new (
252
+ id, document_id, symbol_id, start_line, start_char, end_line, end_char
253
+ )
254
+ SELECT d.id, d.document_id, d.symbol_id, d.start_line, d.start_char, d.end_line, d.end_char
255
+ FROM defn_enclosing_ranges d
256
+ JOIN global_symbols g ON g.id = d.symbol_id
257
+ """)
258
+ _replace_table(conn, "defn_enclosing_ranges", "defn_enclosing_ranges_new")
259
+
260
+
261
+ def _recreate_postprocess_indexes(conn: sqlite3.Connection) -> None:
262
+ """expt-convert indexes are dropped when tables are rebuilt."""
263
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_global_symbols_symbol ON global_symbols(symbol)")
264
+ if conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='mentions' LIMIT 1").fetchone():
265
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_mentions_symbol_id_role ON mentions(symbol_id, role)")
266
+
267
+
268
+ def postprocess_index(db_path, keep_external=None, exclude_globs: tuple[str, ...] | None = None):
269
+ """Shrink index: drop unused columns and prune external symbols by default.
270
+
271
+ Args:
272
+ db_path: Path to the SQLite database
273
+ keep_external: If True, keep symbols without definitions (external libs).
274
+ If None, reads from SCIP_CLI_KEEP_EXTERNAL env var.
275
+ exclude_globs: Repo-relative globs; matching documents are removed entirely.
276
+ """
277
+ from ..sql import configure_bulk_write_connection
278
+
279
+ # Default to False unless explicitly set via parameter or env var
280
+ if keep_external is None:
281
+ keep_external = os.environ.get("SCIP_CLI_KEEP_EXTERNAL", "0") == "1"
282
+
283
+ conn = sqlite3.connect(str(db_path))
284
+ try:
285
+ configure_bulk_write_connection(conn)
286
+ # Rebuild tables first; indexes are recreated once at the end (expt-convert indexes
287
+ # are dropped with table swaps — avoid maintaining them during bulk inserts).
288
+ if exclude_globs:
289
+ _prune_excluded_documents(conn, exclude_globs)
290
+ _trim_unused_columns(conn, keep_external=keep_external)
291
+ _trim_mentions_to_known_symbols(conn)
292
+ _trim_defn_to_known_symbols(conn)
293
+ _recreate_postprocess_indexes(conn)
294
+ conn.commit()
295
+ finally:
296
+ conn.close()