scip-cli 2.10.0__tar.gz → 2.11.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 (158) hide show
  1. {scip_cli-2.10.0 → scip_cli-2.11.0}/PKG-INFO +2 -1
  2. {scip_cli-2.10.0 → scip_cli-2.11.0}/README.md +1 -0
  3. scip_cli-2.11.0/docs/incremental-reindex-roadmap.md +42 -0
  4. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/SKILL.md +3 -1
  5. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/__init__.py +1 -1
  6. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/__main__.py +7 -0
  7. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/reindex.py +18 -1
  8. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/core.py +8 -2
  9. scip_cli-2.11.0/scip_cli/indexing/shards.py +195 -0
  10. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/typescript.py +153 -4
  11. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/tsconfig.py +12 -0
  12. scip_cli-2.11.0/tests/test_incremental_shards.py +227 -0
  13. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_reindex.py +67 -2
  14. {scip_cli-2.10.0 → scip_cli-2.11.0}/.agents/commands/migration-problems.md +0 -0
  15. {scip_cli-2.10.0 → scip_cli-2.11.0}/.agents/rules/agent.mdc +0 -0
  16. {scip_cli-2.10.0 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {scip_cli-2.10.0 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {scip_cli-2.10.0 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
  19. {scip_cli-2.10.0 → scip_cli-2.11.0}/.github/pull_request_template.md +0 -0
  20. {scip_cli-2.10.0 → scip_cli-2.11.0}/.github/workflows/ci.yml +0 -0
  21. {scip_cli-2.10.0 → scip_cli-2.11.0}/.gitignore +0 -0
  22. {scip_cli-2.10.0 → scip_cli-2.11.0}/.pre-commit-config.yaml +0 -0
  23. {scip_cli-2.10.0 → scip_cli-2.11.0}/AGENTS.md +0 -0
  24. {scip_cli-2.10.0 → scip_cli-2.11.0}/CODE_OF_CONDUCT.md +0 -0
  25. {scip_cli-2.10.0 → scip_cli-2.11.0}/CONTRIBUTING.md +0 -0
  26. {scip_cli-2.10.0 → scip_cli-2.11.0}/LICENSE +0 -0
  27. {scip_cli-2.10.0 → scip_cli-2.11.0}/SECURITY.md +0 -0
  28. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/port.md +0 -0
  29. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/conclusions.md +0 -0
  30. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/experiment-summary.md +0 -0
  31. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/go-experiment.md +0 -0
  32. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/go.md +0 -0
  33. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/plan.md +0 -0
  34. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/rust-experiment.md +0 -0
  35. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/rust.md +0 -0
  36. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/summary.md +0 -0
  37. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/zig-experiment.md +0 -0
  38. {scip_cli-2.10.0 → scip_cli-2.11.0}/docs/ports/zig.md +0 -0
  39. {scip_cli-2.10.0 → scip_cli-2.11.0}/pyproject.toml +0 -0
  40. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/__init__.py +0 -0
  41. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/common.py +0 -0
  42. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/file.py +0 -0
  43. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/graph.py +0 -0
  44. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/live.py +0 -0
  45. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/project.py +0 -0
  46. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/sections.py +0 -0
  47. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/symbol.py +0 -0
  48. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/analyze/targets.py +0 -0
  49. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/cache.py +0 -0
  50. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/cli_args.py +0 -0
  51. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/__init__.py +0 -0
  52. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/analyze.py +0 -0
  53. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/code.py +0 -0
  54. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/deps.py +0 -0
  55. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/members.py +0 -0
  56. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/rdeps.py +0 -0
  57. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/refs.py +0 -0
  58. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/search.py +0 -0
  59. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/skill.py +0 -0
  60. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/commands/symbols.py +0 -0
  61. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/config.py +0 -0
  62. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/debug.py +0 -0
  63. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/discover.py +0 -0
  64. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/exclude.py +0 -0
  65. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/__init__.py +0 -0
  66. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/constants.py +0 -0
  67. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/convert.py +0 -0
  68. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/languages.py +0 -0
  69. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/orchestrate.py +0 -0
  70. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/postprocess.py +0 -0
  71. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/indexing/runners.py +0 -0
  72. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/merge.py +0 -0
  73. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/metadata.py +0 -0
  74. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/output.py +0 -0
  75. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/paths.py +0 -0
  76. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/project.py +0 -0
  77. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/queries.py +0 -0
  78. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/scip_tool.py +0 -0
  79. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/scope.py +0 -0
  80. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/session.py +0 -0
  81. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/source.py +0 -0
  82. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/sql.py +0 -0
  83. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/symbols.py +0 -0
  84. {scip_cli-2.10.0 → scip_cli-2.11.0}/scip_cli/targets.py +0 -0
  85. {scip_cli-2.10.0 → scip_cli-2.11.0}/scripts/bench.sh +0 -0
  86. {scip_cli-2.10.0 → scip_cli-2.11.0}/scripts/bench_postprocess.py +0 -0
  87. {scip_cli-2.10.0 → scip_cli-2.11.0}/scripts/build.sh +0 -0
  88. {scip_cli-2.10.0 → scip_cli-2.11.0}/scripts/publish.sh +0 -0
  89. {scip_cli-2.10.0 → scip_cli-2.11.0}/scripts/test.sh +0 -0
  90. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/__init__.py +0 -0
  91. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/analyze_db.py +0 -0
  92. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/bench_db.py +0 -0
  93. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/conftest.py +0 -0
  94. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/e2e_harness.py +0 -0
  95. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixture_catalog.py +0 -0
  96. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/package.json +0 -0
  97. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/__tests__/fixtureOnly.spec.ts +0 -0
  98. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/app/handler.ts +0 -0
  99. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/config.ts +0 -0
  100. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/consumer.ts +0 -0
  101. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/dead.ts +0 -0
  102. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/en.ts +0 -0
  103. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/index.ts +0 -0
  104. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/useLocale.ts +0 -0
  105. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/labels/index.ts +0 -0
  106. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/events/streamTypes.ts +0 -0
  107. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/gateway/index.ts +0 -0
  108. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/helper.ts +0 -0
  109. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useHookA.ts +0 -0
  110. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useHookB.ts +0 -0
  111. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useItems.ts +0 -0
  112. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/index.ts +0 -0
  113. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/integrations/inferenceClient.ts +0 -0
  114. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/lib/handler.ts +0 -0
  115. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/loaders/useInference.ts +0 -0
  116. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/models/MutationAggregate.ts +0 -0
  117. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/pages/panelPage.ts +0 -0
  118. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/rules/applyRule.ts +0 -0
  119. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/types/a.ts +0 -0
  120. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/types/b.ts +0 -0
  121. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/Button.ts +0 -0
  122. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/LazyPanel.ts +0 -0
  123. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/buttonConsumer.ts +0 -0
  124. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/menuModule.ts +0 -0
  125. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/user.ts +0 -0
  126. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/widget.ts +0 -0
  127. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/widgets/OrphanWidget.ts +0 -0
  128. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/fixtures/typescript-project/tsconfig.json +0 -0
  129. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/perf_util.py +0 -0
  130. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_analyze.py +0 -0
  131. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_analyze_graph.py +0 -0
  132. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_analyze_live.py +0 -0
  133. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_analyze_perf.py +0 -0
  134. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_bench_queries.py +0 -0
  135. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_cache.py +0 -0
  136. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_composability.py +0 -0
  137. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_config.py +0 -0
  138. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_discover.py +0 -0
  139. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_e2e.py +0 -0
  140. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_e2e_analyze_patterns.py +0 -0
  141. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_e2e_perf.py +0 -0
  142. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_exclude_globs.py +0 -0
  143. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_index_batching.py +0 -0
  144. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_index_prune.py +0 -0
  145. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_indexer_env.py +0 -0
  146. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_indexer_fallback.py +0 -0
  147. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_merge.py +0 -0
  148. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_metadata.py +0 -0
  149. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_multi_symbol.py +0 -0
  150. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_pure_functions.py +0 -0
  151. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_python_index_integration.py +0 -0
  152. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_qualified_symbols.py +0 -0
  153. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_scip_tool.py +0 -0
  154. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_scope.py +0 -0
  155. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_symbols_freq.py +0 -0
  156. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_targets.py +0 -0
  157. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_tsconfig.py +0 -0
  158. {scip_cli-2.10.0 → scip_cli-2.11.0}/tests/test_typescript_projects.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scip-cli
3
- Version: 2.10.0
3
+ Version: 2.11.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
@@ -266,6 +266,7 @@ scip-cli reindex --tsconfig tsconfig.app.json --tsconfig tsconfig.spec.json
266
266
  scip-cli reindex --exclude '**/*.test.ts' '**/*.spec.ts'
267
267
  scip-cli reindex --exclude # clear persisted excludes
268
268
  scip-cli reindex --fresh # full index; clear metadata.json
269
+ scip-cli reindex --incremental # reuse unchanged project shards (see SKILL for language support)
269
270
  ```
270
271
 
271
272
  `--path` limits which discovered tsconfig **directories** are indexed (prefix match, same idea as query `--path`). `--tsconfig` skips discovery and indexes those `tsconfig*.json` **files** (repeatable; globs are 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). Cannot combine `--path` and `--tsconfig`. **TypeScript only.** Scope and exclude defaults are saved in `metadata.json` next to `index.db` and reused on later `reindex` runs; use `reindex --fresh` to clear them and restore a full index. `--exclude` updates the persisted exclude list; a lone bare `--exclude` clears it.
@@ -245,6 +245,7 @@ scip-cli reindex --tsconfig tsconfig.app.json --tsconfig tsconfig.spec.json
245
245
  scip-cli reindex --exclude '**/*.test.ts' '**/*.spec.ts'
246
246
  scip-cli reindex --exclude # clear persisted excludes
247
247
  scip-cli reindex --fresh # full index; clear metadata.json
248
+ scip-cli reindex --incremental # reuse unchanged project shards (see SKILL for language support)
248
249
  ```
249
250
 
250
251
  `--path` limits which discovered tsconfig **directories** are indexed (prefix match, same idea as query `--path`). `--tsconfig` skips discovery and indexes those `tsconfig*.json` **files** (repeatable; globs are 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). Cannot combine `--path` and `--tsconfig`. **TypeScript only.** Scope and exclude defaults are saved in `metadata.json` next to `index.db` and reused on later `reindex` runs; use `reindex --fresh` to clear them and restore a full index. `--exclude` updates the persisted exclude list; a lone bare `--exclude` clears it.
@@ -0,0 +1,42 @@
1
+ # Incremental reindex roadmap
2
+
3
+ Phased plan to avoid full `scip-typescript` runs on every `reindex`. Shard reuse first; file-level and upstream TSC incremental follow.
4
+
5
+ ## Phase 1 — scip-cli shard reuse (shipped)
6
+
7
+ Per-tsconfig shard cache with content fingerprints. **Shippable:** unit + integration tests in-repo; large monorepos can be validated manually outside the repo (not checked in).
8
+
9
+ - [x] Roadmap doc
10
+ - [x] `shards/manifest.json` in cache dir (fingerprint + part DB path per tsconfig project)
11
+ - [x] `compute_shard_fingerprint()` — tsconfig chain + included source file content hashes + exclude globs
12
+ - [x] `reindex --incremental` — per-project shards (batch size 1), reuse unchanged part DBs
13
+ - [x] Persist shard part DBs under `cache/shards/*.db`
14
+ - [x] Full `reindex` clears shard cache (avoid stale reuse after non-incremental rebuild)
15
+ - [x] `reindex --fresh` clears shard cache
16
+ - [x] Unit tests for fingerprint, manifest merge, and warm `index_typescript` reuse
17
+ - [x] Reindex CLI flag tests (`--incremental`, rejects with `--fresh` / Python)
18
+ - [x] Stderr stats: `Incremental: N shard(s) reused, M reindexed`
19
+ - [x] Update `scip_cli/SKILL.md` and README flag list
20
+
21
+ ## Phase 2 — scip-typescript fork: TSC incremental program
22
+
23
+ - [ ] Fork `sourcegraph/scip-typescript`
24
+ - [ ] `--incremental` / `--ts-build-info-file` CLI flags
25
+ - [ ] `createIncrementalProgram` + `createIncrementalCompilerHost` in `ProjectIndexer`
26
+ - [ ] Default `tsBuildInfoFile` under scip-cli cache dir (not repo root)
27
+ - [ ] scip-cli passes `--ts-build-info-file` when shard is dirty
28
+ - [ ] Benchmark dirty shard: program build time before/after
29
+
30
+ ## Phase 3 — File-level incremental inside a shard
31
+
32
+ - [ ] Document-level upsert in SQLite (replace rows for changed paths; merge is insert-only today)
33
+ - [ ] `reindex --incremental` accepts optional changed-file list (content-hash manifest in cache)
34
+ - [ ] Invalidation: changed files ∪ direct importers (from existing index `mentions`)
35
+ - [ ] scip-typescript fork: skip unchanged files in `ProjectIndexer` loop (`--files` or internal dirty set)
36
+ - [ ] Optional git input via env / scip-atlas (`last_index_commit`, `git diff --name-only`) — not a hard scip-cli dependency
37
+
38
+ ## Phase 4 — Polish
39
+
40
+ - [ ] Dogfood on this repo + optional large-monorepo validation (manual, outside CI)
41
+ - [ ] Submit scip-typescript fork as upstream PR (phase 2, then 3 indexer pieces)
42
+ - [ ] `scripts/bench.sh` scenario for incremental reindex
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scip-cli
3
- description: Read when needing symbols, definitions, refs, members, or SQL health dashboards in TS/JS, Python, Go, or Rust
3
+ description: Read when needing symbol lookup or SQL health dashboards in code.
4
4
  ---
5
5
 
6
6
  TypeScript/JavaScript (.ts, .tsx, .js, .jsx), Python (.py), Go (.go), and Rust (.rs) — not GraphQL, CSS, or other files.
@@ -168,3 +168,5 @@ reindex [--path DIR ...] [--tsconfig FILE_OR_GLOB ...] [--exclude [GLOB ...]] [-
168
168
  `--exclude GLOB` omits matching files from the SQLite index after conversion (repeatable; merged with `excludeGlobs` in `.scip-cli.json`). Passing `--exclude` updates the persisted exclude list; a lone bare `--exclude` (no globs on that flag) clears it — `--exclude foo --exclude` keeps `foo`. 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.
169
169
 
170
170
  `--fresh` ignores persisted `metadata.json` and clears it before indexing unless `--path`, `--tsconfig`, or `--exclude` are set on the same command (use `reindex --fresh` to restore a full index).
171
+
172
+ `--incremental` reuses cached per-project part DBs under `shards/` in the project cache dir (`~/.cache/scip-cli/projects/<slug>/`, alongside `index.db` and `metadata.json`) when inputs are unchanged. **Currently TypeScript only** (Python reindex rejects the flag). Forces one indexer run per discovered project. A plain `reindex` clears the incremental cache; run `reindex --incremental` after an initial full index to benefit. Cannot combine with `--fresh`.
@@ -1,3 +1,3 @@
1
1
  """scip-cli: Fast code intelligence via SCIP indexes."""
2
2
 
3
- __version__ = "2.10.0"
3
+ __version__ = "2.11.0"
@@ -195,6 +195,13 @@ def main() -> None:
195
195
  action="store_true",
196
196
  help="Ignore persisted metadata.json; clear it unless scope/exclude flags are set on this run",
197
197
  )
198
+ reindex_parser.add_argument(
199
+ "--incremental",
200
+ action="store_true",
201
+ help=(
202
+ "Reuse cached per-project index parts when inputs are unchanged (full reindex clears the incremental cache)"
203
+ ),
204
+ )
198
205
  reindex_parser.add_argument(
199
206
  "--with-external",
200
207
  action="store_true",
@@ -11,6 +11,7 @@ from ..cache import (
11
11
  promote_next_index,
12
12
  )
13
13
  from ..indexing import index_project, log_index_complete
14
+ from ..indexing.shards import clear_shard_cache
14
15
  from ..metadata import UNSET, apply_metadata_updates
15
16
  from ..paths import normalize_path_scope
16
17
  from ..project import Language, find_project_root_and_language
@@ -27,6 +28,13 @@ def main(args):
27
28
  tsconfig_args = getattr(args, "tsconfig", None) or []
28
29
  exclude_groups = getattr(args, "exclude", None)
29
30
  fresh = getattr(args, "fresh", False)
31
+ incremental = getattr(args, "incremental", False)
32
+ if incremental and fresh:
33
+ print("Error: reindex --incremental and --fresh cannot be combined", file=sys.stderr)
34
+ sys.exit(1)
35
+ if incremental and lang is not None and lang != Language.TYPESCRIPT:
36
+ print("Error: reindex --incremental is only supported for TypeScript projects", file=sys.stderr)
37
+ sys.exit(1)
30
38
  if path_args and tsconfig_args:
31
39
  print("Error: reindex --path and --tsconfig cannot be combined", file=sys.stderr)
32
40
  sys.exit(1)
@@ -92,6 +100,8 @@ def main(args):
92
100
  exclude_globs=exclude_update,
93
101
  )
94
102
  cleanup_in_progress_index(cache_dir)
103
+ if fresh or not incremental:
104
+ clear_shard_cache(cache_dir)
95
105
  try:
96
106
  # Pass --with-external flag to indexer via environment
97
107
  if getattr(args, "with_external", False):
@@ -100,7 +110,14 @@ def main(args):
100
110
  os.environ["SCIP_CLI_KEEP_EXTERNAL"] = "1"
101
111
 
102
112
  started = time.perf_counter()
103
- _output_db, skipped, total = index_project(root, lang, cache_dir, replace=True, log=False)
113
+ _output_db, skipped, total = index_project(
114
+ root,
115
+ lang,
116
+ cache_dir,
117
+ replace=True,
118
+ log=False,
119
+ incremental=incremental,
120
+ )
104
121
  elapsed_seconds = time.perf_counter() - started
105
122
  except RuntimeError as e:
106
123
  cleanup_in_progress_index(cache_dir)
@@ -97,7 +97,7 @@ def indexer_env(project_root=None):
97
97
  return env
98
98
 
99
99
 
100
- def index_project(root, lang, cache_dir, *, replace=False, log=True):
100
+ def index_project(root, lang, cache_dir, *, replace=False, log=True, incremental=False):
101
101
  """Run the language-specific indexer and convert to DB."""
102
102
  from ..project import Language
103
103
 
@@ -110,7 +110,13 @@ def index_project(root, lang, cache_dir, *, replace=False, log=True):
110
110
  if lang == Language.TYPESCRIPT:
111
111
  projects = typescript_projects(root)
112
112
  output_db, _indexed, skipped, total = index_typescript(
113
- root, cache_dir, projects, env, replace=replace, exclude_globs=exclude_globs
113
+ root,
114
+ cache_dir,
115
+ projects,
116
+ env,
117
+ replace=replace,
118
+ exclude_globs=exclude_globs,
119
+ incremental=incremental,
114
120
  )
115
121
  if log:
116
122
  log_index_complete(
@@ -0,0 +1,195 @@
1
+ """Per-tsconfig shard fingerprints and cached part DBs for incremental reindex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from ..exclude import path_matches_glob
11
+ from ..tsconfig import resolved_exclude, resolved_include_or_files, tsconfig_for_project, walk_tsconfig_chain
12
+
13
+ MANIFEST_VERSION = 1
14
+ MANIFEST_FILENAME = "manifest.json"
15
+ SHARDS_DIRNAME = "shards"
16
+
17
+ _SOURCE_SUFFIXES = frozenset({".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"})
18
+
19
+
20
+ def shards_dir(cache_dir: Path) -> Path:
21
+ return Path(cache_dir) / SHARDS_DIRNAME
22
+
23
+
24
+ def manifest_path(cache_dir: Path) -> Path:
25
+ return shards_dir(cache_dir) / MANIFEST_FILENAME
26
+
27
+
28
+ def shard_key(project: Path) -> str:
29
+ """Stable manifest key for a TypeScript project path."""
30
+ return project.as_posix()
31
+
32
+
33
+ def shard_db_filename(project: Path) -> str:
34
+ stem = shard_key(project).replace("/", "__").replace("\\", "__")
35
+ return f"{stem}.db"
36
+
37
+
38
+ def shard_db_path(cache_dir: Path, project: Path) -> Path:
39
+ return shards_dir(cache_dir) / shard_db_filename(project)
40
+
41
+
42
+ def clear_shard_cache(cache_dir: Path) -> None:
43
+ """Remove persisted shard part DBs and manifest."""
44
+ root = shards_dir(cache_dir)
45
+ if root.is_dir():
46
+ shutil.rmtree(root)
47
+
48
+
49
+ def load_shard_manifest(cache_dir: Path) -> dict[str, dict[str, str]]:
50
+ """Return shard key → {fingerprint, part_db} from manifest.json."""
51
+ path = manifest_path(cache_dir)
52
+ if not path.is_file():
53
+ return {}
54
+ try:
55
+ data = json.loads(path.read_text(encoding="utf-8"))
56
+ except (OSError, json.JSONDecodeError):
57
+ return {}
58
+ if not isinstance(data, dict):
59
+ return {}
60
+ raw_shards = data.get("shards")
61
+ if not isinstance(raw_shards, dict):
62
+ return {}
63
+ out: dict[str, dict[str, str]] = {}
64
+ for key, entry in raw_shards.items():
65
+ if not isinstance(key, str) or not isinstance(entry, dict):
66
+ continue
67
+ fingerprint = entry.get("fingerprint")
68
+ part_db = entry.get("part_db")
69
+ if isinstance(fingerprint, str) and isinstance(part_db, str):
70
+ out[key] = {"fingerprint": fingerprint, "part_db": part_db}
71
+ return out
72
+
73
+
74
+ def save_shard_manifest(cache_dir: Path, shards: dict[str, dict[str, str]]) -> None:
75
+ """Merge shard entries into manifest.json (full reindex clears stale entries)."""
76
+ root = shards_dir(cache_dir)
77
+ root.mkdir(parents=True, exist_ok=True)
78
+ merged = {**load_shard_manifest(cache_dir), **shards}
79
+ payload = {
80
+ "version": MANIFEST_VERSION,
81
+ "shards": {
82
+ key: {"fingerprint": entry["fingerprint"], "part_db": entry["part_db"]}
83
+ for key, entry in sorted(merged.items())
84
+ },
85
+ }
86
+ manifest_path(cache_dir).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
87
+
88
+
89
+ def _hash_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
97
+ def _glob_source_files(base_dir: Path, patterns: list[str]) -> list[Path]:
98
+ found: set[Path] = set()
99
+ for pattern in patterns:
100
+ if Path(pattern).is_absolute():
101
+ candidate = Path(pattern)
102
+ if candidate.is_file() and candidate.suffix.lower() in _SOURCE_SUFFIXES:
103
+ found.add(candidate.resolve())
104
+ continue
105
+ for match in base_dir.glob(pattern):
106
+ if match.is_file() and match.suffix.lower() in _SOURCE_SUFFIXES:
107
+ found.add(match.resolve())
108
+ return sorted(found)
109
+
110
+
111
+ def _tsconfig_excludes_path(tsconfig: Path, file_path: Path) -> bool:
112
+ """Return True when a file is excluded by the leaf tsconfig exclude list."""
113
+ exclude_patterns = resolved_exclude(tsconfig)
114
+ if not exclude_patterns:
115
+ return False
116
+ try:
117
+ relative = file_path.relative_to(tsconfig.parent).as_posix()
118
+ except ValueError:
119
+ return False
120
+ return any(path_matches_glob(relative, pattern) for pattern in exclude_patterns)
121
+
122
+
123
+ def list_project_source_files(root: Path, project: Path) -> list[Path]:
124
+ """Source files that contribute to a shard fingerprint (tsconfig include/files)."""
125
+ root = Path(root).resolve()
126
+ tsconfig = tsconfig_for_project(root, project)
127
+ if tsconfig is None:
128
+ return []
129
+
130
+ include, files = resolved_include_or_files(tsconfig)
131
+ base_dir = tsconfig.parent
132
+ if files is not None:
133
+ candidates = _glob_source_files(base_dir, files)
134
+ elif include is not None:
135
+ candidates = _glob_source_files(base_dir, include)
136
+ else:
137
+ candidates = _glob_source_files(base_dir, ["**/*"])
138
+ return [path for path in candidates if not _tsconfig_excludes_path(tsconfig, path)]
139
+
140
+
141
+ def compute_shard_fingerprint(
142
+ root: Path,
143
+ project: Path,
144
+ *,
145
+ exclude_globs: tuple[str, ...] = (),
146
+ ) -> str:
147
+ """Hash tsconfig chain + indexed sources + exclude globs for one project shard."""
148
+ root = Path(root).resolve()
149
+ tsconfig = tsconfig_for_project(root, project)
150
+ digest = hashlib.sha256()
151
+ digest.update(shard_key(project).encode())
152
+ digest.update(b"\0")
153
+
154
+ if tsconfig is not None:
155
+ for path, _data in walk_tsconfig_chain(tsconfig):
156
+ digest.update(path.as_posix().encode())
157
+ digest.update(b"\0")
158
+ digest.update(path.read_bytes())
159
+ digest.update(b"\0")
160
+
161
+ for path in list_project_source_files(root, project):
162
+ try:
163
+ relative = path.relative_to(root).as_posix()
164
+ except ValueError:
165
+ relative = path.as_posix()
166
+ digest.update(relative.encode())
167
+ digest.update(b"\0")
168
+ digest.update(_hash_file(path).encode())
169
+ digest.update(b"\0")
170
+
171
+ for glob in exclude_globs:
172
+ digest.update(glob.encode())
173
+ digest.update(b"\0")
174
+
175
+ return digest.hexdigest()
176
+
177
+
178
+ def resolve_cached_shard_db(
179
+ cache_dir: Path,
180
+ project: Path,
181
+ fingerprint: str,
182
+ manifest: dict[str, dict[str, str]],
183
+ ) -> Path | None:
184
+ """Return a reusable shard part DB when fingerprint and file both match."""
185
+ key = shard_key(project)
186
+ entry = manifest.get(key)
187
+ if entry is None or entry.get("fingerprint") != fingerprint:
188
+ return None
189
+ candidate = shards_dir(cache_dir) / Path(entry["part_db"]).name
190
+ if candidate.is_file():
191
+ return candidate
192
+ fallback = shard_db_path(cache_dir, project)
193
+ if fallback.is_file():
194
+ return fallback
195
+ return None
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import shutil
6
7
  import sys
7
8
  import tempfile
8
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -24,6 +25,16 @@ from .orchestrate import (
24
25
  ts_batch_limit_display,
25
26
  )
26
27
  from .runners import run_indexer_with_fallback
28
+ from .shards import (
29
+ SHARDS_DIRNAME,
30
+ compute_shard_fingerprint,
31
+ load_shard_manifest,
32
+ resolve_cached_shard_db,
33
+ save_shard_manifest,
34
+ shard_db_filename,
35
+ shard_db_path,
36
+ shard_key,
37
+ )
27
38
 
28
39
 
29
40
  def typescript_projects(root: Path) -> list[Path]:
@@ -125,15 +136,60 @@ def index_ts_projects(
125
136
  return label, db_path, None
126
137
 
127
138
 
128
- def index_typescript(root, cache_dir, projects, env, *, replace=False, exclude_globs: tuple[str, ...] = ()):
139
+ def _persist_shard_db(cache_dir: Path, project: Path, source_db: Path) -> Path:
140
+ """Copy a part DB into the shard cache for incremental reuse."""
141
+ dest = shard_db_path(cache_dir, project)
142
+ dest.parent.mkdir(parents=True, exist_ok=True)
143
+ shutil.copy2(source_db, dest)
144
+ return dest
145
+
146
+
147
+ def _index_shard_batch(
148
+ root: Path,
149
+ cache_dir: Path,
150
+ batch: list[Path],
151
+ work_dir: Path,
152
+ env,
153
+ *,
154
+ exclude_globs: tuple[str, ...],
155
+ output_db: Path | None,
156
+ ) -> tuple[str, Path | None, str | None]:
157
+ label, db_path, error = index_ts_projects(
158
+ root,
159
+ batch,
160
+ work_dir,
161
+ env,
162
+ output_db=output_db,
163
+ exclude_globs=exclude_globs,
164
+ )
165
+ if db_path is None:
166
+ return label, None, error
167
+ if len(batch) == 1:
168
+ _persist_shard_db(cache_dir, batch[0], db_path)
169
+ return label, db_path, None
170
+
171
+
172
+ def index_typescript(
173
+ root,
174
+ cache_dir,
175
+ projects,
176
+ env,
177
+ *,
178
+ replace=False,
179
+ exclude_globs: tuple[str, ...] = (),
180
+ incremental: bool = False,
181
+ ):
129
182
  """Index one or more TypeScript projects and write the merged index.db."""
130
183
  root = Path(root)
131
184
  cache_dir = Path(cache_dir)
132
185
  output_db = index_db_path(cache_dir, replace=replace)
133
186
  workers = index_workers()
134
- batch_size = effective_ts_batch_size(projects)
187
+ batch_size = 1 if incremental else effective_ts_batch_size(projects)
135
188
  batches = batch_projects(projects, batch_size)
136
189
  use_parallel = len(batches) > 1 and workers > 1
190
+ manifest = load_shard_manifest(cache_dir) if incremental else {}
191
+ updated_shards: dict[str, dict[str, str]] = {}
192
+ reused = 0
137
193
 
138
194
  with tempfile.TemporaryDirectory() as tmpdir:
139
195
  tmpdir_path = Path(tmpdir)
@@ -142,14 +198,99 @@ def index_typescript(root, cache_dir, projects, env, *, replace=False, exclude_g
142
198
  total = len(projects)
143
199
  show_progress = total > PROGRESS_LOG_MIN_PROJECTS
144
200
 
145
- if show_progress and use_parallel:
201
+ if incremental and show_progress:
202
+ print(f"Incremental reindex: {total} project shard(s)...", file=sys.stderr)
203
+
204
+ if show_progress and use_parallel and not incremental:
146
205
  batch_desc = ts_batch_limit_display(batch_size, total)
147
206
  print(
148
207
  f"Indexing {total} TypeScript projects ({workers} workers, {batch_desc}; merge is serial)...",
149
208
  file=sys.stderr,
150
209
  )
151
210
 
152
- if use_parallel:
211
+ def _record_shard(project: Path, fingerprint: str) -> None:
212
+ relative = shard_db_filename(project)
213
+ updated_shards[shard_key(project)] = {
214
+ "fingerprint": fingerprint,
215
+ "part_db": f"{SHARDS_DIRNAME}/{relative}",
216
+ }
217
+
218
+ def _try_reuse_shard(project: Path) -> Path | None:
219
+ nonlocal reused
220
+ fingerprint = compute_shard_fingerprint(root, project, exclude_globs=exclude_globs)
221
+ cached = resolve_cached_shard_db(cache_dir, project, fingerprint, manifest)
222
+ if cached is None:
223
+ return None
224
+ reused += 1
225
+ _record_shard(project, fingerprint)
226
+ if show_progress:
227
+ print(f"Reused shard: {shard_key(project)}", file=sys.stderr)
228
+ return cached
229
+
230
+ if incremental:
231
+ ordered_parts: list[tuple[int, Path]] = []
232
+ pending: list[tuple[int, list[Path]]] = []
233
+ for index, batch in enumerate(batches, start=1):
234
+ project = batch[0]
235
+ cached = _try_reuse_shard(project)
236
+ if cached is not None:
237
+ ordered_parts.append((index, cached))
238
+ continue
239
+ pending.append((index, batch))
240
+
241
+ if pending:
242
+ if use_parallel and len(pending) > 1:
243
+ with ThreadPoolExecutor(max_workers=workers) as pool:
244
+ futures = {
245
+ pool.submit(
246
+ _index_shard_batch,
247
+ root,
248
+ cache_dir,
249
+ batch,
250
+ tmpdir_path / f"part-{index}",
251
+ env,
252
+ exclude_globs=exclude_globs,
253
+ output_db=None,
254
+ ): (index, batch)
255
+ for index, batch in pending
256
+ }
257
+ for future in as_completed(futures):
258
+ batch_index, batch = futures[future]
259
+ label, db_path, error = future.result()
260
+ if db_path is None:
261
+ skipped += len(batch)
262
+ print(f"Warning: skipped {label}: {error}", file=sys.stderr)
263
+ else:
264
+ project = batch[0]
265
+ fingerprint = compute_shard_fingerprint(root, project, exclude_globs=exclude_globs)
266
+ _record_shard(project, fingerprint)
267
+ ordered_parts.append((batch_index, db_path))
268
+ if show_progress:
269
+ print(f"Indexed shard: {shard_key(project)}", file=sys.stderr)
270
+ else:
271
+ for index, batch in pending:
272
+ project = batch[0]
273
+ if show_progress:
274
+ print(f"Indexing shard: {shard_key(project)}", file=sys.stderr)
275
+ label, db_path, error = _index_shard_batch(
276
+ root,
277
+ cache_dir,
278
+ batch,
279
+ tmpdir_path / f"part-{index}",
280
+ env,
281
+ exclude_globs=exclude_globs,
282
+ output_db=None,
283
+ )
284
+ if db_path is None:
285
+ skipped += len(batch)
286
+ print(f"Warning: skipped {label}: {error}", file=sys.stderr)
287
+ continue
288
+ fingerprint = compute_shard_fingerprint(root, project, exclude_globs=exclude_globs)
289
+ _record_shard(project, fingerprint)
290
+ ordered_parts.append((index, db_path))
291
+
292
+ part_dbs = [db for _, db in sorted(ordered_parts, key=lambda item: item[0])]
293
+ elif use_parallel:
153
294
  completed = 0
154
295
  with ThreadPoolExecutor(max_workers=workers) as pool:
155
296
  futures = {
@@ -204,4 +345,12 @@ def index_typescript(root, cache_dir, projects, env, *, replace=False, exclude_g
204
345
 
205
346
  finalize_part_dbs(part_dbs, output_db)
206
347
 
348
+ if incremental:
349
+ save_shard_manifest(cache_dir, updated_shards)
350
+ if show_progress or reused:
351
+ print(
352
+ f"Incremental: {reused} shard(s) reused, {len(updated_shards) - reused} reindexed",
353
+ file=sys.stderr,
354
+ )
355
+
207
356
  return output_db, len(part_dbs), skipped, total
@@ -140,6 +140,18 @@ def resolved_allow_js(path: Path) -> bool:
140
140
  return False
141
141
 
142
142
 
143
+ def resolved_exclude(path: Path) -> list[str]:
144
+ """`exclude` from the leaf tsconfig (first entry in the extends chain)."""
145
+ chain = walk_tsconfig_chain(path.resolve())
146
+ if not chain:
147
+ return []
148
+ _file, data = chain[0]
149
+ exclude = data.get("exclude")
150
+ if isinstance(exclude, list) and all(isinstance(item, str) for item in exclude):
151
+ return list(exclude)
152
+ return []
153
+
154
+
143
155
  def resolved_include_or_files(path: Path) -> tuple[list[str] | None, list[str] | None]:
144
156
  """First `include` or `files` found walking leaf → base. The other is None."""
145
157
  for _file, data in walk_tsconfig_chain(path):