scip-cli 2.9.1__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 (159) hide show
  1. {scip_cli-2.9.1 → scip_cli-2.11.0}/PKG-INFO +8 -4
  2. {scip_cli-2.9.1 → scip_cli-2.11.0}/README.md +7 -3
  3. scip_cli-2.11.0/docs/incremental-reindex-roadmap.md +42 -0
  4. {scip_cli-2.9.1 → scip_cli-2.11.0}/pyproject.toml +2 -1
  5. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/SKILL.md +10 -8
  6. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/__init__.py +1 -1
  7. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/__main__.py +21 -1
  8. scip_cli-2.11.0/scip_cli/analyze/common.py +243 -0
  9. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/file.py +54 -47
  10. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/live.py +58 -1
  11. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/project.py +207 -172
  12. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/sections.py +75 -42
  13. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/symbol.py +4 -1
  14. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/analyze.py +42 -4
  15. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/reindex.py +18 -1
  16. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/core.py +8 -2
  17. scip_cli-2.11.0/scip_cli/indexing/shards.py +195 -0
  18. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/typescript.py +153 -4
  19. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/tsconfig.py +12 -0
  20. {scip_cli-2.9.1 → scip_cli-2.11.0}/scripts/bench.sh +1 -1
  21. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/bench_db.py +123 -2
  22. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_analyze.py +107 -10
  23. scip_cli-2.11.0/tests/test_analyze_live.py +71 -0
  24. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_bench_queries.py +41 -2
  25. scip_cli-2.11.0/tests/test_incremental_shards.py +227 -0
  26. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_reindex.py +67 -2
  27. scip_cli-2.9.1/scip_cli/analyze/common.py +0 -133
  28. {scip_cli-2.9.1 → scip_cli-2.11.0}/.agents/commands/migration-problems.md +0 -0
  29. {scip_cli-2.9.1 → scip_cli-2.11.0}/.agents/rules/agent.mdc +0 -0
  30. {scip_cli-2.9.1 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {scip_cli-2.9.1 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {scip_cli-2.9.1 → scip_cli-2.11.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
  33. {scip_cli-2.9.1 → scip_cli-2.11.0}/.github/pull_request_template.md +0 -0
  34. {scip_cli-2.9.1 → scip_cli-2.11.0}/.github/workflows/ci.yml +0 -0
  35. {scip_cli-2.9.1 → scip_cli-2.11.0}/.gitignore +0 -0
  36. {scip_cli-2.9.1 → scip_cli-2.11.0}/.pre-commit-config.yaml +0 -0
  37. {scip_cli-2.9.1 → scip_cli-2.11.0}/AGENTS.md +0 -0
  38. {scip_cli-2.9.1 → scip_cli-2.11.0}/CODE_OF_CONDUCT.md +0 -0
  39. {scip_cli-2.9.1 → scip_cli-2.11.0}/CONTRIBUTING.md +0 -0
  40. {scip_cli-2.9.1 → scip_cli-2.11.0}/LICENSE +0 -0
  41. {scip_cli-2.9.1 → scip_cli-2.11.0}/SECURITY.md +0 -0
  42. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/port.md +0 -0
  43. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/conclusions.md +0 -0
  44. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/experiment-summary.md +0 -0
  45. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/go-experiment.md +0 -0
  46. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/go.md +0 -0
  47. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/plan.md +0 -0
  48. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/rust-experiment.md +0 -0
  49. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/rust.md +0 -0
  50. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/summary.md +0 -0
  51. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/zig-experiment.md +0 -0
  52. {scip_cli-2.9.1 → scip_cli-2.11.0}/docs/ports/zig.md +0 -0
  53. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/__init__.py +0 -0
  54. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/graph.py +0 -0
  55. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/analyze/targets.py +0 -0
  56. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/cache.py +0 -0
  57. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/cli_args.py +0 -0
  58. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/__init__.py +0 -0
  59. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/code.py +0 -0
  60. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/deps.py +0 -0
  61. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/members.py +0 -0
  62. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/rdeps.py +0 -0
  63. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/refs.py +0 -0
  64. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/search.py +0 -0
  65. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/skill.py +0 -0
  66. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/commands/symbols.py +0 -0
  67. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/config.py +0 -0
  68. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/debug.py +0 -0
  69. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/discover.py +0 -0
  70. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/exclude.py +0 -0
  71. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/__init__.py +0 -0
  72. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/constants.py +0 -0
  73. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/convert.py +0 -0
  74. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/languages.py +0 -0
  75. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/orchestrate.py +0 -0
  76. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/postprocess.py +0 -0
  77. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/indexing/runners.py +0 -0
  78. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/merge.py +0 -0
  79. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/metadata.py +0 -0
  80. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/output.py +0 -0
  81. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/paths.py +0 -0
  82. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/project.py +0 -0
  83. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/queries.py +0 -0
  84. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/scip_tool.py +0 -0
  85. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/scope.py +0 -0
  86. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/session.py +0 -0
  87. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/source.py +0 -0
  88. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/sql.py +0 -0
  89. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/symbols.py +0 -0
  90. {scip_cli-2.9.1 → scip_cli-2.11.0}/scip_cli/targets.py +0 -0
  91. {scip_cli-2.9.1 → scip_cli-2.11.0}/scripts/bench_postprocess.py +0 -0
  92. {scip_cli-2.9.1 → scip_cli-2.11.0}/scripts/build.sh +0 -0
  93. {scip_cli-2.9.1 → scip_cli-2.11.0}/scripts/publish.sh +0 -0
  94. {scip_cli-2.9.1 → scip_cli-2.11.0}/scripts/test.sh +0 -0
  95. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/__init__.py +0 -0
  96. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/analyze_db.py +0 -0
  97. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/conftest.py +0 -0
  98. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/e2e_harness.py +0 -0
  99. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixture_catalog.py +0 -0
  100. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/package.json +0 -0
  101. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/__tests__/fixtureOnly.spec.ts +0 -0
  102. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/app/handler.ts +0 -0
  103. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/config.ts +0 -0
  104. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/consumer.ts +0 -0
  105. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/dead.ts +0 -0
  106. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/en.ts +0 -0
  107. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/index.ts +0 -0
  108. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/i18n/useLocale.ts +0 -0
  109. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/domain/labels/index.ts +0 -0
  110. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/events/streamTypes.ts +0 -0
  111. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/gateway/index.ts +0 -0
  112. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/helper.ts +0 -0
  113. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useHookA.ts +0 -0
  114. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useHookB.ts +0 -0
  115. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/hooks/useItems.ts +0 -0
  116. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/index.ts +0 -0
  117. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/integrations/inferenceClient.ts +0 -0
  118. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/lib/handler.ts +0 -0
  119. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/loaders/useInference.ts +0 -0
  120. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/models/MutationAggregate.ts +0 -0
  121. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/pages/panelPage.ts +0 -0
  122. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/rules/applyRule.ts +0 -0
  123. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/types/a.ts +0 -0
  124. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/types/b.ts +0 -0
  125. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/Button.ts +0 -0
  126. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/LazyPanel.ts +0 -0
  127. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/buttonConsumer.ts +0 -0
  128. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/ui/menuModule.ts +0 -0
  129. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/user.ts +0 -0
  130. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/widget.ts +0 -0
  131. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/src/widgets/OrphanWidget.ts +0 -0
  132. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/fixtures/typescript-project/tsconfig.json +0 -0
  133. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/perf_util.py +0 -0
  134. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_analyze_graph.py +0 -0
  135. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_analyze_perf.py +0 -0
  136. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_cache.py +0 -0
  137. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_composability.py +0 -0
  138. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_config.py +0 -0
  139. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_discover.py +0 -0
  140. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_e2e.py +0 -0
  141. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_e2e_analyze_patterns.py +0 -0
  142. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_e2e_perf.py +0 -0
  143. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_exclude_globs.py +0 -0
  144. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_index_batching.py +0 -0
  145. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_index_prune.py +0 -0
  146. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_indexer_env.py +0 -0
  147. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_indexer_fallback.py +0 -0
  148. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_merge.py +0 -0
  149. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_metadata.py +0 -0
  150. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_multi_symbol.py +0 -0
  151. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_pure_functions.py +0 -0
  152. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_python_index_integration.py +0 -0
  153. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_qualified_symbols.py +0 -0
  154. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_scip_tool.py +0 -0
  155. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_scope.py +0 -0
  156. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_symbols_freq.py +0 -0
  157. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_targets.py +0 -0
  158. {scip_cli-2.9.1 → scip_cli-2.11.0}/tests/test_tsconfig.py +0 -0
  159. {scip_cli-2.9.1 → 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.9.1
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
@@ -153,7 +153,7 @@ scip-cli <command> [arguments]
153
153
  - `rdeps <file>` - Find files that depend on a file (`--path`)
154
154
  - `deps <symbol|file>` - Find outbound dependencies (what a symbol or file calls) (`--path`, `--paths-only`)
155
155
  - `members <symbol>` - List members of a class/interface (`--path`)
156
- - `analyze [target]` - SQL health dashboards (`--limit`, `--priority`, `--check`, `--include-tests`). No target: project-wide; directory or file path; symbol name. See [Finding easy wins with `analyze`](#finding-easy-wins-with-analyze).
156
+ - `analyze [target]` - SQL health dashboards (`--limit`, `--per-check-limit`, `--priority`, `--check`, `--include-tests`). No target: project-wide; directory or file path; symbol name. See [Finding easy wins with `analyze`](#finding-easy-wins-with-analyze).
157
157
  - `reindex` - Force re-indexing of the current project (`--path` to limit scope; repeatable)
158
158
  - `skill [path]` - Install or dump the SKILL.md
159
159
 
@@ -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.
@@ -282,16 +283,19 @@ Use `analyze` on the repo itself before broad refactors or agent review — it s
282
283
 
283
284
  ```bash
284
285
  scip-cli analyze --limit 25
286
+ scip-cli analyze --limit 40 --per-check-limit 5 # spread the global budget across checks
285
287
  scip-cli analyze --priority high --limit 25 # dead exports & cycles only
286
288
  scip-cli analyze --check cycles --limit 25 # one named section
287
289
  scip-cli analyze --check dead_files --limit 25
288
290
  ```
289
291
 
292
+ `--limit` is a **global** finding-row budget (truncation markers do not count). `--per-check-limit` (default unlimited) caps each section. Directory analyze spends that same budget on per-file dumps after dashboards.
293
+
290
294
  Sections are tagged `[high]`, `[medium]`, `[low]` and listed in that order.
291
295
 
292
296
  |Tier|Project sections|Action|
293
297
  |---|---|---|
294
- |**high**|Cycles, unreferenced, dead exports, dead files, stale types|Nuke or fix cycles; delete unused; `_` prefix|
298
+ |**high**|Cycles, dead exports, dead files, stale types (unreferenced is omitted when dead exports is on)|Nuke or fix cycles; delete unused; `_` prefix|
295
299
  |**medium**|Same-file only, change surface (file target)|Module-private by usage|
296
300
  |**low**|Test-only consumers, coupling, bottlenecks, hotspots|Noisy on Python (index omits many same-file calls); verify with `rg`|
297
301
 
@@ -302,7 +306,7 @@ Use `--priority high` for a quick gate; `--priority high,medium` adds context. F
302
306
  |Section|Easy pickings|
303
307
  |---|---|
304
308
  |**Cycles**|Import/mention cycles between production files — break the edge or extract shared code|
305
- |**Unreferenced**|No usage in the index at all — delete|
309
+ |**Unreferenced**|Same survivors as dead exports; omitted unless `--check unreferenced`. Empty `rdeps` is usually a SCIP miss — verify with `rg -F STEM` (file basename in `(...)`)|
306
310
  |**Dead exports**|No external refs — delete or `_` prefix|
307
311
  |**Stale types**|Classes/types with no external consumer in the index — verify in-file or type-only use before removing|
308
312
  |**Same-file only**|Used only inside defining file — rename to `_`|
@@ -132,7 +132,7 @@ scip-cli <command> [arguments]
132
132
  - `rdeps <file>` - Find files that depend on a file (`--path`)
133
133
  - `deps <symbol|file>` - Find outbound dependencies (what a symbol or file calls) (`--path`, `--paths-only`)
134
134
  - `members <symbol>` - List members of a class/interface (`--path`)
135
- - `analyze [target]` - SQL health dashboards (`--limit`, `--priority`, `--check`, `--include-tests`). No target: project-wide; directory or file path; symbol name. See [Finding easy wins with `analyze`](#finding-easy-wins-with-analyze).
135
+ - `analyze [target]` - SQL health dashboards (`--limit`, `--per-check-limit`, `--priority`, `--check`, `--include-tests`). No target: project-wide; directory or file path; symbol name. See [Finding easy wins with `analyze`](#finding-easy-wins-with-analyze).
136
136
  - `reindex` - Force re-indexing of the current project (`--path` to limit scope; repeatable)
137
137
  - `skill [path]` - Install or dump the SKILL.md
138
138
 
@@ -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.
@@ -261,16 +262,19 @@ Use `analyze` on the repo itself before broad refactors or agent review — it s
261
262
 
262
263
  ```bash
263
264
  scip-cli analyze --limit 25
265
+ scip-cli analyze --limit 40 --per-check-limit 5 # spread the global budget across checks
264
266
  scip-cli analyze --priority high --limit 25 # dead exports & cycles only
265
267
  scip-cli analyze --check cycles --limit 25 # one named section
266
268
  scip-cli analyze --check dead_files --limit 25
267
269
  ```
268
270
 
271
+ `--limit` is a **global** finding-row budget (truncation markers do not count). `--per-check-limit` (default unlimited) caps each section. Directory analyze spends that same budget on per-file dumps after dashboards.
272
+
269
273
  Sections are tagged `[high]`, `[medium]`, `[low]` and listed in that order.
270
274
 
271
275
  |Tier|Project sections|Action|
272
276
  |---|---|---|
273
- |**high**|Cycles, unreferenced, dead exports, dead files, stale types|Nuke or fix cycles; delete unused; `_` prefix|
277
+ |**high**|Cycles, dead exports, dead files, stale types (unreferenced is omitted when dead exports is on)|Nuke or fix cycles; delete unused; `_` prefix|
274
278
  |**medium**|Same-file only, change surface (file target)|Module-private by usage|
275
279
  |**low**|Test-only consumers, coupling, bottlenecks, hotspots|Noisy on Python (index omits many same-file calls); verify with `rg`|
276
280
 
@@ -281,7 +285,7 @@ Use `--priority high` for a quick gate; `--priority high,medium` adds context. F
281
285
  |Section|Easy pickings|
282
286
  |---|---|
283
287
  |**Cycles**|Import/mention cycles between production files — break the edge or extract shared code|
284
- |**Unreferenced**|No usage in the index at all — delete|
288
+ |**Unreferenced**|Same survivors as dead exports; omitted unless `--check unreferenced`. Empty `rdeps` is usually a SCIP miss — verify with `rg -F STEM` (file basename in `(...)`)|
285
289
  |**Dead exports**|No external refs — delete or `_` prefix|
286
290
  |**Stale types**|Classes/types with no external consumer in the index — verify in-file or type-only use before removing|
287
291
  |**Same-file only**|Used only inside defining file — rename to `_`|
@@ -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
@@ -54,9 +54,10 @@ testpaths = ["tests"]
54
54
  python_files = ["test_*.py"]
55
55
  python_classes = ["Test*"]
56
56
  python_functions = ["test_*"]
57
- addopts = "-q --tb=line --no-header"
57
+ addopts = "-q --tb=line --no-header -m 'not benchmark'"
58
58
  markers = [
59
59
  "integration: uses the shared indexed TS fixture (requires npx / scip-typescript)",
60
+ "benchmark: scaled :memory: query timings (run via scripts/bench.sh only)",
60
61
  ]
61
62
 
62
63
  [tool.basedpyright]
@@ -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.
@@ -64,7 +64,7 @@ Commands emit one record per line on stdout; warnings and progress go to stderr.
64
64
  |Goal|Pipeline|
65
65
  |---|---|
66
66
  |Blast radius of a file|`scip-cli rdeps file.ts \|xargs -I{} scip-cli symbols {}`|
67
- |Pre-change / health briefing|`scip-cli analyze` or `scip-cli analyze path/to/file.ts` or `scip-cli analyze SymbolName`|
67
+ |Pre-change / health briefing|`scip-cli analyze --limit 40 --per-check-limit 5` (or a file / symbol)|
68
68
  |Files that import a symbol|`scip-cli refs Foo --paths-only`|
69
69
  |Symbols in referencing files|`scip-cli refs Foo --paths-only \|xargs -I{} scip-cli symbols {}` (barrel files may have no symbols; prefer `search Foo --paths-only` for definition files)|
70
70
  |Find classes, list members|`scip-cli search Handler --kind class --names-only \|xargs -I{} scip-cli members {}`|
@@ -133,7 +133,7 @@ Returns `startLine:endLine kind name` for each member. Members are found via SCI
133
133
  ### analyze
134
134
 
135
135
  ```bash
136
- analyze [--limit N] [--path PATH] [--include-tests] [--priority LEVEL] [--check NAME] [target]
136
+ analyze [--limit N] [--per-check-limit N] [--path PATH] [--include-tests] [--priority LEVEL] [--check NAME] [target]
137
137
  ```
138
138
 
139
139
  |Target|Output|
@@ -143,9 +143,11 @@ analyze [--limit N] [--path PATH] [--include-tests] [--priority LEVEL] [--check
143
143
  |**file**|Scoped project dashboards for that file + per-file sections + top symbols by external consumers|
144
144
  |**symbol**|Symbol pressure, consumers, dependencies, affected|
145
145
 
146
- Sections are ordered **high → medium → low**. `[high]` cycles, unreferenced, dead exports, dead files, stale types; `[medium]` same-file-only, change surface (file); `[low]` test-only consumers (noisy on Python), coupling, bottlenecks, hotspots.
146
+ Sections are ordered **high → medium → low**. `[high]` cycles, dead exports, dead files, stale types (unreferenced is skipped when dead exports is on — same survivors; file-target unreferenced-in-file is skipped when dead-in-file is on); `[medium]` same-file-only, change surface (file); `[low]` test-only consumers (noisy on Python), coupling, bottlenecks, hotspots.
147
147
 
148
- `--limit` caps **result rows across the whole run** (default 20); remaining checks are skipped once the cap is reached. `(none)` sections and section headers do not count.
148
+ `--limit` caps **result rows across the whole run** (default 20); remaining checks are skipped once the cap is reached. `(none)` sections, headers, and `… truncated` / `[note]` lines do not count as findings. `--per-check-limit N` additionally caps each section (default: unlimited) so one noisy check cannot spend the whole budget. Agents: `analyze --limit 40 --per-check-limit 5`.
149
+
150
+ Directory `analyze some/dir` spends the **same global `--limit`** on per-file sections after dashboards (max 30 files). Prefer project/dir dashboards, then `analyze` one file.
149
151
 
150
152
  `--priority high` or `--priority high,medium` (also `1`/`2`/`3`) skips lower tiers.
151
153
 
@@ -153,9 +155,7 @@ Sections are ordered **high → medium → low**. `[high]` cycles, unreferenced,
153
155
 
154
156
  Directory detection uses the filesystem when present, otherwise an indexed path prefix. `--path` narrows ambiguous file/symbol resolution only (not directory scope — pass the dir as `target`).
155
157
 
156
- **Dogfood loop:** `reindex` → `analyze --limit 25` → `analyze scip_cli` or `analyze scip_cli/queries.py` on suspects. Skips test paths in project-wide and directory runs (`tests/`, `*.test.*`, `*.spec.*`); `--include-tests` to include them. File-target analyze always includes that file.
157
-
158
- **Easy pickings:** **Cycles**, **dead exports**, and **dead files** (production paths) — cross-file cleanup. **Stale types** — types with no external refs in the index. Ignore `analyze/*` section helpers in dead exports. “Dead” = no refs from _other_ files in the index (empty `rdeps` for files), not `vulture`. Sections with hits print a short false-positive caveat; `export const` arrows often look like dead files because SCIP indexes the module only.
158
+ **Easy pickings:** **Cycles**, **dead exports**, and **dead files** (production paths) — cross-file cleanup. **Stale types** — types with no external refs in the index. Ignore `analyze/*` section helpers in dead exports. “Dead” = no refs from _other_ files in the index, not `vulture`. Empty `scip-cli rdeps` on a hit is usually a SCIP miss (`require()`, named imports, barrels), not proof unused. Unreferenced is omitted when dead exports is on (same survivors). Sections with hits print one `Warn:`; for each row run `rg -F STEM` where STEM is the **file basename inside `(...)`**, no extension — ignore hits in that same file. Do not `rg` the symbol name.
159
159
 
160
160
  ### reindex
161
161
 
@@ -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.9.1"
3
+ __version__ = "2.11.0"
@@ -11,7 +11,13 @@ import sys
11
11
  from collections.abc import Callable
12
12
 
13
13
  from . import __version__
14
- from .cli_args import add_limit_argument, add_names_only_argument, add_path_argument, add_paths_only_argument
14
+ from .cli_args import (
15
+ add_limit_argument,
16
+ add_names_only_argument,
17
+ add_path_argument,
18
+ add_paths_only_argument,
19
+ positive_int,
20
+ )
15
21
  from .commands import analyze, code, deps, members, rdeps, refs, reindex, search, skill, symbols
16
22
  from .symbols import SymbolKind
17
23
 
@@ -131,6 +137,13 @@ def main() -> None:
131
137
  )
132
138
  add_path_argument(analyze_parser)
133
139
  add_limit_argument(analyze_parser, default=20, help_suffix="result rows total")
140
+ analyze_parser.add_argument(
141
+ "--per-check-limit",
142
+ type=positive_int(),
143
+ default=None,
144
+ metavar="N",
145
+ help="Max rows per check (default: unlimited). --limit remains the global row budget",
146
+ )
134
147
  analyze_parser.add_argument(
135
148
  "--include-tests",
136
149
  action="store_true",
@@ -182,6 +195,13 @@ def main() -> None:
182
195
  action="store_true",
183
196
  help="Ignore persisted metadata.json; clear it unless scope/exclude flags are set on this run",
184
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
+ )
185
205
  reindex_parser.add_argument(
186
206
  "--with-external",
187
207
  action="store_true",
@@ -0,0 +1,243 @@
1
+ """Shared SQL helpers and formatting for analyze checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Iterable
7
+
8
+ from ..sql import debug_execute
9
+ from ..symbols import extract_leaf_name, is_module_symbol
10
+
11
+ DEFAULT_LIMIT = 20
12
+ # Cap rows scanned when post-filtering SQL (filters drop many SCIP false hits).
13
+ ANALYZE_MAX_SCAN_ROWS = 10_000
14
+
15
+
16
+ def collect_until_limit(
17
+ limit: int,
18
+ fetch_page: Callable[[int, int], list[object]],
19
+ accept: Callable[[object], str | None],
20
+ *,
21
+ page_size: int | None = None,
22
+ max_scan: int = ANALYZE_MAX_SCAN_ROWS,
23
+ ) -> list[str]:
24
+ """Fetch SQL pages until `limit` rows pass `accept`, or data is exhausted.
25
+
26
+ Use only when a check post-filters SQL rows (analyze_noise, live heuristics, …).
27
+ Checks that format every SQL row as-is should use a single ``LIMIT ?`` instead.
28
+ """
29
+ batch = page_size or max(limit, 50)
30
+ lines: list[str] = []
31
+ offset = 0
32
+ scanned = 0
33
+ while len(lines) < limit:
34
+ rows = fetch_page(batch, offset)
35
+ if not rows:
36
+ break
37
+ for row in rows:
38
+ scanned += 1
39
+ if scanned > max_scan:
40
+ return lines
41
+ line = accept(row)
42
+ if line is None:
43
+ continue
44
+ lines.append(line)
45
+ if len(lines) >= limit:
46
+ return lines
47
+ if len(rows) < batch:
48
+ break
49
+ offset += len(rows)
50
+ return lines
51
+
52
+
53
+ def collect_from_producer(
54
+ limit: int,
55
+ produce: Callable[[int], list[str]],
56
+ accept: Callable[[str], str | None],
57
+ *,
58
+ max_scan: int = ANALYZE_MAX_SCAN_ROWS,
59
+ ) -> list[str]:
60
+ """Fill ``limit`` rows from a capped producer that can return more candidates on retry.
61
+
62
+ Used when results are not SQL-paginated (e.g. graph cycle search). ``produce(cap)``
63
+ must return a deterministic prefix as ``cap`` grows; already-seen strings are skipped.
64
+ """
65
+ lines: list[str] = []
66
+ seen: set[str] = set()
67
+ scanned = 0
68
+ fetch_cap = limit
69
+ while len(lines) < limit and fetch_cap <= max_scan:
70
+ candidates = produce(fetch_cap)
71
+ if not candidates:
72
+ break
73
+ for candidate in candidates:
74
+ if candidate in seen:
75
+ continue
76
+ seen.add(candidate)
77
+ scanned += 1
78
+ if scanned > max_scan:
79
+ return lines
80
+ line = accept(candidate)
81
+ if line is None:
82
+ continue
83
+ lines.append(line)
84
+ if len(lines) >= limit:
85
+ return lines
86
+ if len(candidates) < fetch_cap:
87
+ break
88
+ fetch_cap += limit
89
+ return lines
90
+
91
+
92
+ def collect_from_iterable(
93
+ limit: int,
94
+ candidates: Iterable[str],
95
+ accept: Callable[[str], str | None],
96
+ *,
97
+ max_scan: int = ANALYZE_MAX_SCAN_ROWS,
98
+ ) -> list[str]:
99
+ """Filter an in-memory candidate list until ``limit`` rows pass ``accept``."""
100
+ lines: list[str] = []
101
+ scanned = 0
102
+ for candidate in candidates:
103
+ scanned += 1
104
+ if scanned > max_scan:
105
+ break
106
+ line = accept(candidate)
107
+ if line is None:
108
+ continue
109
+ lines.append(line)
110
+ if len(lines) >= limit:
111
+ break
112
+ return lines
113
+
114
+
115
+ # Definition document for a symbol (our trimmed schema uses defn_enclosing_ranges).
116
+ SYM_DEF_JOIN = """
117
+ JOIN defn_enclosing_ranges sym_def ON sym_def.symbol_id = gs.id
118
+ JOIN documents def_d ON sym_def.document_id = def_d.id
119
+ """
120
+
121
+
122
+ def fetch_all(db, sql: str, params=()):
123
+ return debug_execute(db, sql, params).fetchall()
124
+
125
+
126
+ def fetch_one(db, sql: str, params=()):
127
+ return debug_execute(db, sql, params).fetchone()
128
+
129
+
130
+ def short_name(symbol: str) -> str:
131
+ if is_module_symbol(symbol):
132
+ return "(module)"
133
+ leaf = extract_leaf_name(symbol)
134
+ if leaf:
135
+ return leaf
136
+ if symbol.endswith("/"):
137
+ return "(module)"
138
+ return symbol.split("/")[-1][:60]
139
+
140
+
141
+ def is_test_path(relative_path: str) -> bool:
142
+ """True for common test layout paths and test-like filenames."""
143
+ p = relative_path.replace("\\", "/")
144
+ lower = p.lower()
145
+ if lower.startswith(("tests/", "test/")):
146
+ return True
147
+ if "/tests/" in lower or "/test/" in lower or "/__tests__/" in lower:
148
+ return True
149
+ if "/__fixtures__/" in lower or "/__mocks__/" in lower:
150
+ return True
151
+ name = lower.rsplit("/", 1)[-1]
152
+ if ".test." in name or ".spec." in name or ".mocha." in name:
153
+ return True
154
+ stem = name.rsplit(".", 1)[0]
155
+ if stem.endswith("_test") or stem.endswith("_spec"):
156
+ return True
157
+ if "test-fixture" in name or "test_fixture" in name or "test-mocks" in name:
158
+ return True
159
+ if name.startswith("test_") and name.endswith(".py"):
160
+ return True
161
+ return name == "conftest.py"
162
+
163
+
164
+ def is_dynamic_loader_path(relative_path: str) -> bool:
165
+ """True for migration files typically loaded by filename; SCIP has no import edge."""
166
+ p = relative_path.replace("\\", "/").lower()
167
+ return "/migrations/" in p
168
+
169
+
170
+ def is_python_main_entrypoint(relative_path: str, symbol: str) -> bool:
171
+ """True for main() in __main__.py — argparse entrypoints SCIP does not link to callers."""
172
+ if short_name(symbol) != "main":
173
+ return False
174
+ return relative_path.replace("\\", "/").endswith("__main__.py")
175
+
176
+
177
+ def analyze_noise(relative_path: str, symbol: str, *, include_tests: bool = False) -> bool:
178
+ """True for rows that clutter analyze dashboards (test paths, module-private helpers)."""
179
+ if not include_tests and is_test_path(relative_path):
180
+ return True
181
+ if short_name(symbol).startswith("_"):
182
+ return True
183
+ if is_python_main_entrypoint(relative_path, symbol):
184
+ return True
185
+ if is_dynamic_loader_path(relative_path):
186
+ return True
187
+ if short_name(symbol) in {"<constructor>", "constructor"}:
188
+ return True
189
+ return is_analyze_dashboard_export(relative_path, symbol)
190
+
191
+
192
+ _ANALYZE_DASHBOARD_SUFFIXES = (
193
+ "analyze/project.py",
194
+ "analyze/file.py",
195
+ "analyze/symbol.py",
196
+ )
197
+
198
+
199
+ def is_analyze_dashboard_export(relative_path: str, symbol: str) -> bool:
200
+ """Section runner functions in analyze/* (same-file only, look like dead exports)."""
201
+ path = relative_path.replace("\\", "/")
202
+ if not any(path.endswith(suffix) for suffix in _ANALYZE_DASHBOARD_SUFFIXES):
203
+ return False
204
+ name = short_name(symbol)
205
+ return "()." in symbol or name.endswith(")")
206
+
207
+
208
+ def is_component_props_type(symbol: str) -> bool:
209
+ """React-style Props interfaces — rarely referenced outside the component file."""
210
+ name = short_name(symbol)
211
+ return bool(name and name.endswith("Props") and "#" in symbol.split("/")[-1])
212
+
213
+
214
+ def stale_type_noise(relative_path: str, symbol: str, consumers: int) -> bool:
215
+ """Dataclass-style types with no SCIP consumers (typing-only)."""
216
+ if is_component_props_type(symbol):
217
+ return True
218
+ if consumers > 0:
219
+ return False
220
+ name = short_name(symbol)
221
+ if not name or name == "(module)" or not name[0].isupper():
222
+ return False
223
+ path = relative_path.replace("\\", "/")
224
+ return path.endswith(("config.py", "scope.py", "analyze/targets.py"))
225
+
226
+
227
+ def file_pair_noise(file1: str, file2: str, *, include_tests: bool = False) -> bool:
228
+ if include_tests:
229
+ return False
230
+ return is_test_path(file1) or is_test_path(file2)
231
+
232
+
233
+ def cycle_path_noise(cycle_line: str, *, include_tests: bool = False) -> bool:
234
+ if include_tests:
235
+ return False
236
+ parts = re.split(r"\s<->\s|\s->\s", cycle_line)
237
+ return any(is_test_path(part.strip()) for part in parts if part.strip())
238
+
239
+
240
+ def section(title: str, lines: list[str], *, preface: str | None = None) -> tuple[str, list[str], str | None]:
241
+ if not lines:
242
+ return title, ["(none)"], None
243
+ return title, lines, preface