scip-cli 2.0.0__tar.gz → 2.2.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.
- {scip_cli-2.0.0 → scip_cli-2.2.0}/.cursor/rules/agent.mdc +75 -20
- {scip_cli-2.0.0 → scip_cli-2.2.0}/PKG-INFO +12 -5
- {scip_cli-2.0.0 → scip_cli-2.2.0}/README.md +11 -4
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/SKILL.md +2 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/__init__.py +1 -1
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/common.py +26 -3
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/file.py +53 -17
- scip_cli-2.2.0/scip_cli/analyze/graph.py +133 -0
- scip_cli-2.2.0/scip_cli/analyze/live.py +139 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/project.py +109 -66
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/analyze.py +3 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/refs.py +4 -2
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/output.py +17 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/queries.py +21 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/symbols.py +26 -0
- scip_cli-2.2.0/scripts/publish.sh +96 -0
- scip_cli-2.2.0/scripts/verify_rovetia_reports.py +128 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/analyze_db.py +63 -0
- scip_cli-2.2.0/tests/fixture_catalog.py +44 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/en.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/i18n/useLocale.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/domain/labels/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/events/streamTypes.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/gateway/index.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useHookA.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useHookB.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/hooks/useItems.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/integrations/inferenceClient.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/loaders/useInference.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/pages/panelPage.ts +4 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/rules/applyRule.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/types/a.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/types/b.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/Button.ts +7 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/LazyPanel.ts +3 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/buttonConsumer.ts +5 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/ui/menuModule.ts +1 -0
- scip_cli-2.2.0/tests/fixtures/sample-project/src/widgets/OrphanWidget.ts +3 -0
- scip_cli-2.2.0/tests/perf_util.py +45 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_analyze.py +38 -4
- scip_cli-2.2.0/tests/test_analyze_graph.py +66 -0
- scip_cli-2.2.0/tests/test_analyze_perf.py +39 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_e2e.py +2 -2
- scip_cli-2.2.0/tests/test_e2e_analyze_patterns.py +176 -0
- scip_cli-2.2.0/tests/test_e2e_perf.py +53 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_pure_functions.py +9 -1
- scip_cli-2.0.0/scripts/publish.sh +0 -32
- scip_cli-2.0.0/tests/fixture_catalog.py +0 -17
- {scip_cli-2.0.0 → scip_cli-2.2.0}/.gitignore +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/.pre-commit-config.yaml +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/LICENSE +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/pyproject.toml +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/__main__.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/__init__.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/sections.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/symbol.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/analyze/targets.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/cache.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/cli_args.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/__init__.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/code.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/members.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/rdeps.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/reindex.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/search.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/skill.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/commands/symbols.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/config.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/debug.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/discover.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/indexing.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/merge.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/paths.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/project.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/scip_tool.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/scope.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/session.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/source.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/sql.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scip_cli/targets.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scripts/build.sh +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/scripts/test.sh +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/__init__.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/conftest.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/e2e_harness.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/package.json +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/app/handler.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/config.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/consumer.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/dead.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/helper.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/index.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/user.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/src/widget.ts +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/fixtures/sample-project/tsconfig.json +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_cache.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_composability.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_config.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_discover.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_index_prune.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_indexer_env.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_merge.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_multi_symbol.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_qualified_symbols.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_reindex.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_scip_tool.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_scope.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_targets.py +0 -0
- {scip_cli-2.0.0 → scip_cli-2.2.0}/tests/test_typescript_projects.py +0 -0
|
@@ -7,26 +7,32 @@ alwaysApply: true
|
|
|
7
7
|
|
|
8
8
|
**Work from repo root.** CLI usage, flags, and user gotchas: `scip-cli skill` (source: `scip_cli/SKILL.md`). Read once per task — do not duplicate SKILL here.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**Almost always:** bare `scip-cli` on PATH must be **this checkout live** (`pip install -e ".[dev]"`). All examples use `scip-cli` — never `.venv/bin/scip-cli`, `python -m scip_cli`, or other paths. Only swap to PyPI briefly to smoke-test a release.
|
|
11
11
|
|
|
12
12
|
## Bootstrap
|
|
13
13
|
|
|
14
|
+
From repo root — keep this mapping for the whole dev session:
|
|
15
|
+
|
|
14
16
|
```bash
|
|
15
|
-
python -m venv .venv && source .venv/bin/activate
|
|
16
17
|
pip install -e ".[dev]"
|
|
18
|
+
scip-cli --version # must match scip_cli/__init__.py
|
|
17
19
|
pre-commit install
|
|
18
|
-
scip-cli skill
|
|
20
|
+
scip-cli skill ~/.claude/skills/scip-cli/
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
After publishing: `pip install scip-cli` → smoke-test → `pip install -e ".[dev]"` to restore local.
|
|
24
|
+
|
|
25
|
+
Optional venv for isolation only: create/activate, then same `pip install -e ".[dev]"` — `scip-cli` still runs from PATH, not a venv-relative path.
|
|
26
|
+
|
|
21
27
|
Node.js + `npx` required for integration tests (`scip-typescript`). Optional: `SCIP_CLI_DEBUG=1` (SQL to stderr, truncated).
|
|
22
28
|
|
|
23
29
|
## Lint / test
|
|
24
30
|
|
|
25
31
|
| Task | Command |
|
|
26
32
|
|------|---------|
|
|
27
|
-
| Full suite |
|
|
28
|
-
| E2e loop |
|
|
29
|
-
| Lint / format |
|
|
33
|
+
| Full suite | `pytest` |
|
|
34
|
+
| E2e loop | `pytest tests/test_e2e.py tests/test_e2e_analyze_patterns.py` |
|
|
35
|
+
| Lint / format | `ruff check .` / `ruff format .` |
|
|
30
36
|
|
|
31
37
|
Pre-commit: ruff + ruff-format + full pytest. E2e before commit on command changes.
|
|
32
38
|
|
|
@@ -35,11 +41,11 @@ Pre-commit: ruff + ruff-format + full pytest. E2e before commit on command chang
|
|
|
35
41
|
**Dogfood on the repo itself** — faster and more honest than spawning reviewers:
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
scip-cli reindex
|
|
45
|
+
scip-cli analyze --limit 25
|
|
46
|
+
scip-cli analyze --priority high --limit 25 # dead code & cycles only
|
|
47
|
+
scip-cli analyze scip_cli --limit 15 # directory
|
|
48
|
+
scip-cli analyze scip_cli/queries.py --limit 20 # file (+ scoped project)
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
Interpretation: README § *Finding easy wins with analyze*. Target = omit (project), directory, file, or symbol — not `--path` for scope.
|
|
@@ -63,9 +69,10 @@ When you or a review subagent finds a repo issue — bug, dead code, cycle, risk
|
|
|
63
69
|
|
|
64
70
|
If **yes** (the signal is in `documents`, `mentions`, `global_symbols`, `defn_enclosing_ranges`):
|
|
65
71
|
|
|
66
|
-
1. **
|
|
67
|
-
2. **
|
|
68
|
-
3. **
|
|
72
|
+
1. **Red on the fixture** — minimal generic repro + e2e test (see **TDD → External project feedback**). Do not name real customer repos in tests.
|
|
73
|
+
2. **Extend `analyze`** — new or tuned SQL/heuristic in `scip_cli/analyze/`, priority in `sections.py`; unit tests only for graph/SQL plumbing (`analyze_db.py`, `test_analyze_graph.py`).
|
|
74
|
+
3. **Dogfood** — `reindex` on this repo (or re-run fixture e2e) → confirm signal appears or false positive is filtered.
|
|
75
|
+
4. **Then** fix production code in the other repo (or document a known blind spot).
|
|
69
76
|
|
|
70
77
|
Only skip step 1 when the issue is **outside the index** (runtime, CLI UX, docs drift, git, env, concurrency, test harness).
|
|
71
78
|
|
|
@@ -103,16 +110,64 @@ SQLite has **line ranges** (`defn_enclosing_ranges`) so LOC is cheap. That does
|
|
|
103
110
|
|
|
104
111
|
**Rule:** size-only checks stay **low or file-drill-down**; never high/medium unless paired with a **structural smell** (cycle, dead export, zero production consumers). Prefer enriching existing low-tier lines (e.g. show `loc` on bottlenecks) over a new dashboard section.
|
|
105
112
|
|
|
113
|
+
## SQL / analyze benchmarks
|
|
114
|
+
|
|
115
|
+
The e2e fixture and this repo's own index are **small** — every query looks fast here. That hides blowups on real projects (40MB+ indexes, dense graphs).
|
|
116
|
+
|
|
117
|
+
**Before merging new or heavily changed SQL** (`queries.py`, `analyze/*`):
|
|
118
|
+
|
|
119
|
+
1. Time it against **1–2 existing checks** on a **large cached index** you already have (not the fixture):
|
|
120
|
+
```bash
|
|
121
|
+
python3 -u -c "
|
|
122
|
+
import sqlite3, time, sys
|
|
123
|
+
from pathlib import Path
|
|
124
|
+
sys.path.insert(0, '.')
|
|
125
|
+
from scip_cli.analyze import project as p
|
|
126
|
+
from scip_cli.sql import configure_read_connection
|
|
127
|
+
db_path = Path.home() / '.cache/scip-cli/projects/<project-slug>/index.db'
|
|
128
|
+
db = sqlite3.connect(f'file:{db_path}?mode=ro', uri=True)
|
|
129
|
+
configure_read_connection(db)
|
|
130
|
+
for name, fn in [('hotspots', p.hotspots), ('cycles', p.cycles), ('dead_exports', p.dead_exports)]:
|
|
131
|
+
t = time.perf_counter(); fn(db, limit=25); print(f'{name}: {(time.perf_counter()-t)*1000:.0f}ms')
|
|
132
|
+
"
|
|
133
|
+
```
|
|
134
|
+
2. Flag anything **notably slower** than peers — orders of magnitude or multi-second, not a few ms drift.
|
|
135
|
+
3. Swap in your new/changed check for one of the rows; keep a fast peer (e.g. `hotspots`) and a heavy peer (e.g. `dead_exports`).
|
|
136
|
+
|
|
137
|
+
**In CI:** `tests/test_e2e_perf.py` (whole commands) and `tests/test_analyze_perf.py` (per-check on mini DB) catch gross regressions on the tiny fixture only — they **will not** catch large-repo OOMs. Use the manual benchmark above for that.
|
|
138
|
+
|
|
106
139
|
## TDD
|
|
107
140
|
|
|
108
|
-
|
|
109
|
-
2. **Green** — `scip_cli/commands/*.py`.
|
|
110
|
-
3. **Refactor** — unit tests for pure helpers only.
|
|
141
|
+
### Default: extend the shared fixture
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
**One indexed TS project** — `tests/fixtures/sample-project/` — indexed once per session via `indexed_fixture` (`conftest.py`). Stable names live in `tests/fixture_catalog.py`.
|
|
144
|
+
|
|
145
|
+
1. **Red** — add a **minimal** `.ts` file (or tweak an existing one) that reproduces the SCIP shape; assert in `tests/test_e2e.py` or `tests/test_e2e_analyze_patterns.py` using `cli` / `open_index_db` + analyze helpers.
|
|
146
|
+
2. **Green** — fix `scip_cli/` (commands, `analyze/*`, `queries.py`, …).
|
|
147
|
+
3. **Refactor** — only if a pure helper deserves isolation.
|
|
148
|
+
|
|
149
|
+
- **E2e** (`cli` / `indexed_fixture`): in-process `main()` or direct Python API on the **real** index — default for behavior.
|
|
114
150
|
- **No subprocess** for CLI tests; no `reindex` in e2e (mutates `~/.cache/scip-cli`).
|
|
115
|
-
- New symbols:
|
|
151
|
+
- New symbols/paths: fixture file + `fixture_catalog.py` constant — never hardcode paths in test bodies.
|
|
152
|
+
|
|
153
|
+
### External project feedback → fixture (mandatory workflow)
|
|
154
|
+
|
|
155
|
+
When dogfooding **another repo** surfaces a bug or false positive (analyze noise, dead-export mistake, cycle, stale type, refs mismatch):
|
|
156
|
+
|
|
157
|
+
1. **Capture the pattern, not the project** — note the SCIP shape (module `/`, export alias `foo0:`, lazy import, `Props#`, barrel cycle, …). Do **not** copy real repo names, paths, or filenames into tests or docs.
|
|
158
|
+
2. **Red on the fixture** — smallest TS repro under `tests/fixtures/sample-project/` with generic names (`LazyPanel`, `evictItem`, `OrphanWidget`, …). E2e test proves the bug against **real `scip-typescript`** output.
|
|
159
|
+
3. **Green** — fix analyze heuristics / SQL / CLI; dogfood on this repo if applicable.
|
|
160
|
+
4. **Verify** — `pytest tests/test_e2e_analyze_patterns.py` (or full `pytest`); only then trim redundant tests.
|
|
161
|
+
|
|
162
|
+
**Prefer fixture e2e over fake SQLite graphs.** `tests/analyze_db.py` (`AnalyzeDbBuilder`, `:memory:`) is for SQL/dashboard **plumbing** and graph primitives (`test_analyze_graph.py`, `mini_codebase_db`) — not for reproducing production SCIP symbol shapes. Hand-built symbols often diverge from scip-typescript (path format, `0:` aliases, module rows) and can pass while production still breaks.
|
|
163
|
+
|
|
164
|
+
| Use fixture e2e | Use `analyze_db` unit |
|
|
165
|
+
|-----------------|----------------------|
|
|
166
|
+
| False positives / analyze checks from real projects | Tarjan, edge-type SQL, `run_all` section wiring |
|
|
167
|
+
| Any behavior that depends on indexer symbol strings | Noise filters with one or two synthetic rows |
|
|
168
|
+
| CLI commands (`refs`, `search`, `analyze` output) | Merge/index prune tests |
|
|
169
|
+
|
|
170
|
+
If both layers help during development, **keep both until e2e passes**, then delete duplicate in-memory repros — fixture is the source of truth.
|
|
116
171
|
|
|
117
172
|
## Codebase map (contributors)
|
|
118
173
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scip-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -62,17 +62,24 @@ scip-cli skill
|
|
|
62
62
|
pip install scip-cli
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
**Local development**
|
|
65
|
+
**Local development** — almost always keep bare `scip-cli` mapped to this checkout via editable install. You edit the repo; the CLI you run is live code, not a frozen PyPI copy.
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
cd scip-cli
|
|
69
|
-
python -m venv .venv
|
|
70
|
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
71
69
|
pip install -e ".[dev]"
|
|
72
70
|
scip-cli --version
|
|
73
71
|
```
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
All docs and examples use `scip-cli` (never `.venv/bin/scip-cli` or `python -m scip_cli`). After publishing, briefly `pip install scip-cli` to smoke-test PyPI, then `pip install -e ".[dev]"` again.
|
|
74
|
+
|
|
75
|
+
Optional venv for isolation — activate it, run the same editable install, still invoke `scip-cli` on PATH:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m venv .venv && source .venv/bin/activate
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Tests and lint: `pytest`, `ruff check .` from the same environment.
|
|
76
83
|
|
|
77
84
|
### 2. Install prerequisites (optional)
|
|
78
85
|
|
|
@@ -42,17 +42,24 @@ scip-cli skill
|
|
|
42
42
|
pip install scip-cli
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
**Local development**
|
|
45
|
+
**Local development** — almost always keep bare `scip-cli` mapped to this checkout via editable install. You edit the repo; the CLI you run is live code, not a frozen PyPI copy.
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
cd scip-cli
|
|
49
|
-
python -m venv .venv
|
|
50
|
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
51
49
|
pip install -e ".[dev]"
|
|
52
50
|
scip-cli --version
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
All docs and examples use `scip-cli` (never `.venv/bin/scip-cli` or `python -m scip_cli`). After publishing, briefly `pip install scip-cli` to smoke-test PyPI, then `pip install -e ".[dev]"` again.
|
|
54
|
+
|
|
55
|
+
Optional venv for isolation — activate it, run the same editable install, still invoke `scip-cli` on PATH:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m venv .venv && source .venv/bin/activate
|
|
59
|
+
pip install -e ".[dev]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Tests and lint: `pytest`, `ruff check .` from the same environment.
|
|
56
63
|
|
|
57
64
|
### 2. Install prerequisites (optional)
|
|
58
65
|
|
|
@@ -7,6 +7,8 @@ TypeScript/JavaScript (.ts, .tsx, .js, .jsx) and Python (.py) — not GraphQL, C
|
|
|
7
7
|
|
|
8
8
|
All commands are sub-commands of `scip-cli`. Run from the project root.
|
|
9
9
|
|
|
10
|
+
**Contributors:** keep `pip install -e .` (or `pip install -e ".[dev]"` in this repo) so bare `scip-cli` on PATH runs your live checkout — not a venv-relative path.
|
|
11
|
+
|
|
10
12
|
## Quick Decision Guide
|
|
11
13
|
|
|
12
14
|
| Question | Use | What you get |
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
|
|
7
7
|
from ..sql import debug_execute
|
|
8
|
-
from ..symbols import extract_leaf_name
|
|
8
|
+
from ..symbols import extract_leaf_name, is_module_symbol
|
|
9
9
|
|
|
10
10
|
DEFAULT_LIMIT = 20
|
|
11
11
|
|
|
@@ -25,8 +25,14 @@ def fetch_one(db, sql: str, params=()):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def short_name(symbol: str) -> str:
|
|
28
|
+
if is_module_symbol(symbol):
|
|
29
|
+
return "(module)"
|
|
28
30
|
leaf = extract_leaf_name(symbol)
|
|
29
|
-
|
|
31
|
+
if leaf:
|
|
32
|
+
return leaf
|
|
33
|
+
if symbol.endswith("/"):
|
|
34
|
+
return "(module)"
|
|
35
|
+
return symbol.split("/")[-1][:60]
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def is_test_path(relative_path: str) -> bool:
|
|
@@ -53,10 +59,19 @@ def is_cli_entrypoint(relative_path: str, symbol: str) -> bool:
|
|
|
53
59
|
return path == "scip_cli/__main__.py" or "/commands/" in path
|
|
54
60
|
|
|
55
61
|
|
|
62
|
+
def is_generated_analyze_path(relative_path: str) -> bool:
|
|
63
|
+
p = relative_path.replace("\\", "/")
|
|
64
|
+
if "/types/prisma/" in p:
|
|
65
|
+
return True
|
|
66
|
+
return p.endswith("types/resolvers.ts")
|
|
67
|
+
|
|
68
|
+
|
|
56
69
|
def analyze_noise(relative_path: str, symbol: str, *, include_tests: bool = False) -> bool:
|
|
57
70
|
"""True for rows that clutter analyze dashboards (test paths, module-private helpers)."""
|
|
58
71
|
if not include_tests and is_test_path(relative_path):
|
|
59
72
|
return True
|
|
73
|
+
if is_generated_analyze_path(relative_path):
|
|
74
|
+
return True
|
|
60
75
|
if short_name(symbol).startswith("_"):
|
|
61
76
|
return True
|
|
62
77
|
if is_cli_entrypoint(relative_path, symbol):
|
|
@@ -80,12 +95,20 @@ def is_analyze_dashboard_export(relative_path: str, symbol: str) -> bool:
|
|
|
80
95
|
return "()." in symbol or name.endswith(")")
|
|
81
96
|
|
|
82
97
|
|
|
98
|
+
def is_component_props_type(symbol: str) -> bool:
|
|
99
|
+
"""React-style Props interfaces — rarely referenced outside the component file."""
|
|
100
|
+
name = short_name(symbol)
|
|
101
|
+
return bool(name and name.endswith("Props") and "#" in symbol.split("/")[-1])
|
|
102
|
+
|
|
103
|
+
|
|
83
104
|
def stale_type_noise(relative_path: str, symbol: str, consumers: int) -> bool:
|
|
84
105
|
"""Dataclass-style types with no SCIP consumers (typing-only)."""
|
|
106
|
+
if is_component_props_type(symbol):
|
|
107
|
+
return True
|
|
85
108
|
if consumers > 0:
|
|
86
109
|
return False
|
|
87
110
|
name = short_name(symbol)
|
|
88
|
-
if not name or not name[0].isupper():
|
|
111
|
+
if not name or name == "(module)" or not name[0].isupper():
|
|
89
112
|
return False
|
|
90
113
|
path = relative_path.replace("\\", "/")
|
|
91
114
|
return path.endswith(("config.py", "scope.py", "analyze/targets.py"))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from .common import DEFAULT_LIMIT, SYM_DEF_JOIN, analyze_noise, fetch_all, short_name
|
|
6
|
+
from .live import LiveIndex
|
|
6
7
|
from .sections import Check, Priority, run_checks
|
|
7
8
|
from .symbol import symbol_pressure
|
|
8
9
|
|
|
@@ -82,10 +83,11 @@ def file_consumers(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[s
|
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
def unreferenced_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
86
|
+
live = LiveIndex(db)
|
|
85
87
|
rows = fetch_all(
|
|
86
88
|
db,
|
|
87
89
|
"""
|
|
88
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
90
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
89
91
|
FROM global_symbols gs
|
|
90
92
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
91
93
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -104,14 +106,18 @@ def unreferenced_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) ->
|
|
|
104
106
|
""",
|
|
105
107
|
(relative_path, limit),
|
|
106
108
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
lines = []
|
|
110
|
+
for symbol, start, end, def_doc_id in rows:
|
|
111
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
112
|
+
continue
|
|
113
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
114
|
+
continue
|
|
115
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
116
|
+
return lines
|
|
112
117
|
|
|
113
118
|
|
|
114
119
|
def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
120
|
+
live = LiveIndex(db)
|
|
115
121
|
rows = fetch_all(
|
|
116
122
|
db,
|
|
117
123
|
"""
|
|
@@ -135,18 +141,22 @@ def same_file_only_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -
|
|
|
135
141
|
""",
|
|
136
142
|
(relative_path, limit),
|
|
137
143
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
lines = []
|
|
145
|
+
for symbol, start, end in rows:
|
|
146
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
147
|
+
continue
|
|
148
|
+
if live.same_file_export_noise(symbol):
|
|
149
|
+
continue
|
|
150
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
151
|
+
return lines
|
|
143
152
|
|
|
144
153
|
|
|
145
154
|
def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
155
|
+
live = LiveIndex(db)
|
|
146
156
|
rows = fetch_all(
|
|
147
157
|
db,
|
|
148
158
|
"""
|
|
149
|
-
SELECT gs.symbol, der.start_line, der.end_line
|
|
159
|
+
SELECT gs.symbol, der.start_line, der.end_line, def_d.id
|
|
150
160
|
FROM global_symbols gs
|
|
151
161
|
JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
|
|
152
162
|
JOIN documents def_d ON der.document_id = def_d.id
|
|
@@ -164,11 +174,14 @@ def dead_in_file(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
164
174
|
""",
|
|
165
175
|
(relative_path, limit),
|
|
166
176
|
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
lines = []
|
|
178
|
+
for symbol, start, end, def_doc_id in rows:
|
|
179
|
+
if analyze_noise(relative_path, symbol, include_tests=True):
|
|
180
|
+
continue
|
|
181
|
+
if live.dead_export_noise(symbol, def_doc_id):
|
|
182
|
+
continue
|
|
183
|
+
lines.append(f"{short_name(symbol)} {start + 1}:{end + 1}")
|
|
184
|
+
return lines
|
|
172
185
|
|
|
173
186
|
|
|
174
187
|
def imports_summary(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
@@ -240,6 +253,29 @@ def coupling_for(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str
|
|
|
240
253
|
return [f"{other} shared={shared}" for other, shared in rows]
|
|
241
254
|
|
|
242
255
|
|
|
256
|
+
def count_file_importers(db, relative_path: str) -> int:
|
|
257
|
+
from ..queries import get_file_symbols, get_importer_paths
|
|
258
|
+
|
|
259
|
+
symbols = get_file_symbols(db, relative_path)
|
|
260
|
+
if not symbols:
|
|
261
|
+
return 0
|
|
262
|
+
symbol_ids = [row[0] for row in symbols]
|
|
263
|
+
return len(get_importer_paths(db, symbol_ids, relative_path))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def dead_export_rdeps_warning(db, relative_path: str, *, limit: int = DEFAULT_LIMIT) -> str | None:
|
|
267
|
+
"""Warn when dead/unreferenced exports coexist with file importers (SCIP symbol split)."""
|
|
268
|
+
if not dead_in_file(db, relative_path, limit=limit) and not unreferenced_in_file(db, relative_path, limit=limit):
|
|
269
|
+
return None
|
|
270
|
+
importer_count = count_file_importers(db, relative_path)
|
|
271
|
+
if importer_count == 0:
|
|
272
|
+
return None
|
|
273
|
+
return (
|
|
274
|
+
f"Warning: {relative_path} has {importer_count} importer(s) but lists dead/unreferenced exports — "
|
|
275
|
+
"default-export, lazy(), or object-alias false positives are likely; verify with rg or rdeps"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
243
279
|
def top_symbol_pressure(db, relative_path: str, limit: int = DEFAULT_LIMIT) -> list[str]:
|
|
244
280
|
"""Pressure metrics for the most-consumed exports in a file."""
|
|
245
281
|
cap = min(5, limit)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""File dependency graph helpers for analyze (cycle detection)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from ..symbols import cycle_runtime_edge_sql
|
|
8
|
+
from .common import fetch_all
|
|
9
|
+
|
|
10
|
+
FILE_EDGES_SQL = f"""
|
|
11
|
+
SELECT DISTINCT d1.relative_path AS from_file, d2.relative_path AS to_file
|
|
12
|
+
FROM mentions m
|
|
13
|
+
JOIN chunks c ON m.chunk_id = c.id
|
|
14
|
+
JOIN documents d1 ON c.document_id = d1.id
|
|
15
|
+
JOIN defn_enclosing_ranges der ON m.symbol_id = der.symbol_id
|
|
16
|
+
JOIN documents d2 ON der.document_id = d2.id
|
|
17
|
+
JOIN global_symbols gs ON gs.id = der.symbol_id
|
|
18
|
+
WHERE d1.id != d2.id AND m.role != 1
|
|
19
|
+
AND {cycle_runtime_edge_sql()}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_file_edges(db) -> list[tuple[str, str]]:
|
|
24
|
+
return fetch_all(db, FILE_EDGES_SQL)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _tarjan_sccs(graph: dict[str, list[str]], nodes: set[str]) -> list[list[str]]:
|
|
28
|
+
index = 0
|
|
29
|
+
stack: list[str] = []
|
|
30
|
+
on_stack: set[str] = set()
|
|
31
|
+
indices: dict[str, int] = {}
|
|
32
|
+
lowlink: dict[str, int] = {}
|
|
33
|
+
sccs: list[list[str]] = []
|
|
34
|
+
|
|
35
|
+
def strongconnect(vertex: str) -> None:
|
|
36
|
+
nonlocal index
|
|
37
|
+
indices[vertex] = index
|
|
38
|
+
lowlink[vertex] = index
|
|
39
|
+
index += 1
|
|
40
|
+
stack.append(vertex)
|
|
41
|
+
on_stack.add(vertex)
|
|
42
|
+
for neighbor in graph.get(vertex, ()):
|
|
43
|
+
if neighbor not in indices:
|
|
44
|
+
strongconnect(neighbor)
|
|
45
|
+
lowlink[vertex] = min(lowlink[vertex], lowlink[neighbor])
|
|
46
|
+
elif neighbor in on_stack:
|
|
47
|
+
lowlink[vertex] = min(lowlink[vertex], indices[neighbor])
|
|
48
|
+
if lowlink[vertex] == indices[vertex]:
|
|
49
|
+
component: list[str] = []
|
|
50
|
+
while True:
|
|
51
|
+
w = stack.pop()
|
|
52
|
+
on_stack.remove(w)
|
|
53
|
+
component.append(w)
|
|
54
|
+
if w == vertex:
|
|
55
|
+
break
|
|
56
|
+
sccs.append(component)
|
|
57
|
+
|
|
58
|
+
for node in nodes:
|
|
59
|
+
if node not in indices:
|
|
60
|
+
strongconnect(node)
|
|
61
|
+
return sccs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cycles_in_scc(
|
|
65
|
+
graph: dict[str, list[str]],
|
|
66
|
+
scc_nodes: list[str],
|
|
67
|
+
*,
|
|
68
|
+
max_depth: int,
|
|
69
|
+
limit: int,
|
|
70
|
+
) -> list[str]:
|
|
71
|
+
if len(scc_nodes) <= 2:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
scc = set(scc_nodes)
|
|
75
|
+
subgraph: dict[str, list[str]] = defaultdict(list)
|
|
76
|
+
for src in scc:
|
|
77
|
+
for dst in graph.get(src, ()):
|
|
78
|
+
if dst in scc:
|
|
79
|
+
subgraph[src].append(dst)
|
|
80
|
+
|
|
81
|
+
found: dict[tuple[str, ...], str] = {}
|
|
82
|
+
|
|
83
|
+
def record(path: list[str]) -> None:
|
|
84
|
+
key = min(tuple(path[i:] + path[:i]) for i in range(len(path)))
|
|
85
|
+
if key not in found:
|
|
86
|
+
found[key] = " -> ".join([*path, path[0]])
|
|
87
|
+
|
|
88
|
+
for origin in sorted(scc):
|
|
89
|
+
stack: list[tuple[str, list[str]]] = [(origin, [origin])]
|
|
90
|
+
while stack:
|
|
91
|
+
node, path = stack.pop()
|
|
92
|
+
if len(path) > max_depth:
|
|
93
|
+
continue
|
|
94
|
+
for nxt in subgraph.get(node, ()):
|
|
95
|
+
if nxt == origin and len(path) >= 2:
|
|
96
|
+
record(path)
|
|
97
|
+
if len(found) >= limit:
|
|
98
|
+
return sorted(found.values())[:limit]
|
|
99
|
+
elif nxt not in path:
|
|
100
|
+
stack.append((nxt, [*path, nxt]))
|
|
101
|
+
return sorted(found.values())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def find_longer_cycles(
|
|
105
|
+
edges: list[tuple[str, str]],
|
|
106
|
+
*,
|
|
107
|
+
max_depth: int = 8,
|
|
108
|
+
limit: int,
|
|
109
|
+
) -> list[str]:
|
|
110
|
+
"""Find directed cycles with 3+ distinct files (2-node pairs use the SQL two-way check)."""
|
|
111
|
+
if limit <= 0 or not edges:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
graph: dict[str, list[str]] = defaultdict(list)
|
|
115
|
+
nodes: set[str] = set()
|
|
116
|
+
for src, dst in edges:
|
|
117
|
+
graph[src].append(dst)
|
|
118
|
+
nodes.add(src)
|
|
119
|
+
nodes.add(dst)
|
|
120
|
+
|
|
121
|
+
found: dict[tuple[str, ...], str] = {}
|
|
122
|
+
for component in _tarjan_sccs(graph, nodes):
|
|
123
|
+
if len(component) <= 2:
|
|
124
|
+
continue
|
|
125
|
+
for line in _cycles_in_scc(graph, component, max_depth=max_depth, limit=limit):
|
|
126
|
+
path = line.split(" -> ")
|
|
127
|
+
body = path[:-1]
|
|
128
|
+
key = min(tuple(body[i:] + body[:i]) for i in range(len(body)))
|
|
129
|
+
if key not in found:
|
|
130
|
+
found[key] = line
|
|
131
|
+
if len(found) >= limit:
|
|
132
|
+
return sorted(found.values())[:limit]
|
|
133
|
+
return sorted(found.values())[:limit]
|