sourcecode 1.58.0__tar.gz → 1.60.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.
- {sourcecode-1.58.0 → sourcecode-1.60.0}/CHANGELOG.md +35 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/PKG-INFO +5 -5
- {sourcecode-1.58.0 → sourcecode-1.60.0}/README.md +4 -4
- {sourcecode-1.58.0 → sourcecode-1.60.0}/pyproject.toml +1 -1
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/cli.py +18 -17
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/license.py +1 -1
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/repository_ir.py +106 -2
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/__init__.py +3 -3
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/config.py +30 -4
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/consent.py +18 -21
- {sourcecode-1.58.0 → sourcecode-1.60.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/.gitignore +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/.ruff.toml +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/LICENSE +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/SECURITY.md +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/raw +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/integration_detector.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.58.0 → sourcecode-1.60.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.60.0] — 2026-06-29
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Anonymous telemetry is now on by default (opt-out).** Previously telemetry
|
|
7
|
+
was strictly opt-in and disabled until the user accepted a y/N consent prompt.
|
|
8
|
+
It now defaults to **on** so usage metrics flow without an extra step, while
|
|
9
|
+
staying fully anonymous — no source code, paths, file names, secrets, or
|
|
10
|
+
repository content are ever collected. The first interactive run shows a
|
|
11
|
+
one-time **notice** (not a prompt) explaining what is collected and how to
|
|
12
|
+
disable. Disable any time with `sourcecode telemetry disable`,
|
|
13
|
+
`SOURCECODE_TELEMETRY=0`, or the `DO_NOT_TRACK=1` convention. With no explicit
|
|
14
|
+
choice, telemetry still defaults to **off in CI** (no human to see the notice);
|
|
15
|
+
an explicit opt-in is honored everywhere. README, `--help`, and
|
|
16
|
+
`docs/privacy.md` updated accordingly.
|
|
17
|
+
|
|
18
|
+
## [1.59.0] — 2026-06-19
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Event topology now recognizes generic-wrapper event publishers.**
|
|
22
|
+
`publishEvent(new SaveServiceEvent<>(obj))` and explicit forms like
|
|
23
|
+
`new SaveServiceEvent<Order>(obj)` were silently dropped: the publisher-edge
|
|
24
|
+
regex required the constructor `(` to immediately follow the class name, so any
|
|
25
|
+
diamond `<>` or type argument broke the match. `repo-ir` / event-topology then
|
|
26
|
+
reported no producers for the whole generic `*ServiceEvent` family (e.g. OpenMRS
|
|
27
|
+
`SaveServiceEvent`/`VoidServiceEvent`/`RetireServiceEvent`). The inline and
|
|
28
|
+
two-step publish scans now skip an optional (incl. nested) generic argument list.
|
|
29
|
+
- **`impact-chain` now resolves intra-class method callers.** A query on a
|
|
30
|
+
private/helper method (e.g. `OrderServiceImpl#stopOrder`) found zero direct
|
|
31
|
+
callers and degraded to a wrong class-level expansion, because method-to-method
|
|
32
|
+
calls inside a single class produced no graph edge — only class-level `calls`
|
|
33
|
+
edges existed. The relation builder now emits method-level `calls` edges for
|
|
34
|
+
bare `m(...)` and `this.m(...)` invocations whose target is a sibling method of
|
|
35
|
+
the same class (string/comment-aware, overload-safe, no self-loops). The two
|
|
36
|
+
defects were found dogfooding the tool on OpenMRS issue #6197.
|
|
37
|
+
|
|
3
38
|
## [1.58.0] — 2026-06-19
|
|
4
39
|
|
|
5
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.60.0
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.
|
|
117
|
+
# sourcecode 1.59.0
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -742,7 +742,7 @@ All outputs include:
|
|
|
742
742
|
|
|
743
743
|
## Telemetry
|
|
744
744
|
|
|
745
|
-
Anonymous, opt-
|
|
745
|
+
Anonymous, **on by default (opt-out)**. Collects: version, OS, commands, flags, duration, repo size range, errors. No source code, paths, secrets, or output content. A one-time notice is shown on first interactive run.
|
|
746
746
|
|
|
747
747
|
```bash
|
|
748
748
|
sourcecode telemetry status
|
|
@@ -750,7 +750,7 @@ sourcecode telemetry enable
|
|
|
750
750
|
sourcecode telemetry disable
|
|
751
751
|
```
|
|
752
752
|
|
|
753
|
-
|
|
753
|
+
Disable any time: `export SOURCECODE_TELEMETRY=0` (or `DO_NOT_TRACK=1`)
|
|
754
754
|
|
|
755
755
|
---
|
|
756
756
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.
|
|
79
|
+
# sourcecode 1.59.0
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -704,7 +704,7 @@ All outputs include:
|
|
|
704
704
|
|
|
705
705
|
## Telemetry
|
|
706
706
|
|
|
707
|
-
Anonymous, opt-
|
|
707
|
+
Anonymous, **on by default (opt-out)**. Collects: version, OS, commands, flags, duration, repo size range, errors. No source code, paths, secrets, or output content. A one-time notice is shown on first interactive run.
|
|
708
708
|
|
|
709
709
|
```bash
|
|
710
710
|
sourcecode telemetry status
|
|
@@ -712,7 +712,7 @@ sourcecode telemetry enable
|
|
|
712
712
|
sourcecode telemetry disable
|
|
713
713
|
```
|
|
714
714
|
|
|
715
|
-
|
|
715
|
+
Disable any time: `export SOURCECODE_TELEMETRY=0` (or `DO_NOT_TRACK=1`)
|
|
716
716
|
|
|
717
717
|
---
|
|
718
718
|
|
|
@@ -584,7 +584,7 @@ try:
|
|
|
584
584
|
except Exception:
|
|
585
585
|
pass
|
|
586
586
|
|
|
587
|
-
telemetry_app = typer.Typer(help="Manage anonymous telemetry (opt-
|
|
587
|
+
telemetry_app = typer.Typer(help="Manage anonymous telemetry (on by default; opt-out).", rich_markup_mode="rich")
|
|
588
588
|
app.add_typer(telemetry_app, name="telemetry")
|
|
589
589
|
|
|
590
590
|
mcp_app = typer.Typer(help="MCP integration: setup, status, serve, remove.", rich_markup_mode="rich")
|
|
@@ -597,18 +597,18 @@ auth_app = typer.Typer(help="Authentication: status, logout.", rich_markup_mode=
|
|
|
597
597
|
app.add_typer(auth_app, name="auth")
|
|
598
598
|
|
|
599
599
|
|
|
600
|
-
def
|
|
601
|
-
"""Show first-run
|
|
600
|
+
def _maybe_show_telemetry_notice() -> None:
|
|
601
|
+
"""Show first-run telemetry notice once, on interactive TTYs only.
|
|
602
|
+
|
|
603
|
+
Telemetry is on by default (opt-out). We inform rather than ask, then
|
|
604
|
+
mark the notice as shown so it appears only once.
|
|
605
|
+
"""
|
|
602
606
|
try:
|
|
603
|
-
from sourcecode.telemetry.config import has_been_asked,
|
|
604
|
-
from sourcecode.telemetry.consent import
|
|
607
|
+
from sourcecode.telemetry.config import has_been_asked, mark_asked
|
|
608
|
+
from sourcecode.telemetry.consent import show_first_run_notice
|
|
605
609
|
if not has_been_asked():
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if enabled:
|
|
609
|
-
typer.echo("Telemetry enabled. Thank you. Disable: sourcecode telemetry disable", err=True)
|
|
610
|
-
else:
|
|
611
|
-
typer.echo("Telemetry disabled. Enable anytime: sourcecode telemetry enable", err=True)
|
|
610
|
+
show_first_run_notice()
|
|
611
|
+
mark_asked()
|
|
612
612
|
except Exception:
|
|
613
613
|
pass
|
|
614
614
|
|
|
@@ -1020,9 +1020,9 @@ def main(
|
|
|
1020
1020
|
sourcecode /path/to/repo --compact analyze specific path
|
|
1021
1021
|
sourcecode --agent agent-optimized output (full detail)
|
|
1022
1022
|
"""
|
|
1023
|
-
# First-run
|
|
1023
|
+
# First-run telemetry notice (skip for telemetry/version/config subcommands)
|
|
1024
1024
|
if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
|
|
1025
|
-
|
|
1025
|
+
_maybe_show_telemetry_notice()
|
|
1026
1026
|
_maybe_show_mcp_hint()
|
|
1027
1027
|
|
|
1028
1028
|
# When a subcommand is invoked, skip the main analysis.
|
|
@@ -3342,12 +3342,12 @@ def telemetry_status() -> None:
|
|
|
3342
3342
|
enabled = is_enabled()
|
|
3343
3343
|
asked = has_been_asked()
|
|
3344
3344
|
status = "enabled" if enabled else "disabled"
|
|
3345
|
-
typer.echo(f"Telemetry: {status}")
|
|
3345
|
+
typer.echo(f"Telemetry: {status} (on by default; opt-out)")
|
|
3346
3346
|
if not asked:
|
|
3347
|
-
typer.echo(" (
|
|
3347
|
+
typer.echo(" (first-run notice not yet shown — will show on next run)")
|
|
3348
3348
|
typer.echo(f" Config: {config_file_path()}")
|
|
3349
|
-
typer.echo(" Disable
|
|
3350
|
-
typer.echo(" Or set env var:
|
|
3349
|
+
typer.echo(" Disable: sourcecode telemetry disable")
|
|
3350
|
+
typer.echo(" Or set env var: SOURCECODE_TELEMETRY=0 (or DO_NOT_TRACK=1)")
|
|
3351
3351
|
|
|
3352
3352
|
|
|
3353
3353
|
@telemetry_app.command("enable")
|
|
@@ -3369,6 +3369,7 @@ def telemetry_disable() -> None:
|
|
|
3369
3369
|
from sourcecode.telemetry.config import set_enabled
|
|
3370
3370
|
set_enabled(False)
|
|
3371
3371
|
typer.echo("Telemetry disabled. No data will be collected or sent.")
|
|
3372
|
+
typer.echo("Telemetry is on by default; this opt-out is remembered.")
|
|
3372
3373
|
typer.echo("Re-enable at any time: sourcecode telemetry enable")
|
|
3373
3374
|
|
|
3374
3375
|
|
|
@@ -372,7 +372,7 @@ _init()
|
|
|
372
372
|
# ---------------------------------------------------------------------------
|
|
373
373
|
|
|
374
374
|
def _emit_telemetry(event: str, **kw: object) -> None:
|
|
375
|
-
"""Best-effort telemetry emit. Respects opt-
|
|
375
|
+
"""Best-effort telemetry emit. Respects the user's opt-out; never raises or blocks."""
|
|
376
376
|
try:
|
|
377
377
|
from sourcecode import telemetry as _tel
|
|
378
378
|
_tel.record(event, **kw) # type: ignore[arg-type]
|
|
@@ -347,12 +347,17 @@ _SPRING_OTHER: frozenset[str] = frozenset({
|
|
|
347
347
|
"@MatrixParam", "@CookieParam", "@Context",
|
|
348
348
|
})
|
|
349
349
|
|
|
350
|
-
|
|
350
|
+
# Optional generic type args between the event class name and its constructor
|
|
351
|
+
# parens — diamond `<>` or explicit `<Order>` / `<Map<String,Integer>>`.
|
|
352
|
+
# Required so `publishEvent(new SaveServiceEvent<>(obj))` (generic event wrappers,
|
|
353
|
+
# e.g. OpenMRS *ServiceEvent family) is recognised as a publisher edge.
|
|
354
|
+
_GENERIC_ARGS = r'(?:<[^<>;{}()]*(?:<[^<>;{}()]*>[^<>;{}()]*)*>)?'
|
|
355
|
+
_PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*' + _GENERIC_ARGS + r'\s*[(\{]')
|
|
351
356
|
|
|
352
357
|
# Two-step publish: SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)
|
|
353
358
|
# Used when event is created before passing to publishEvent (common pattern).
|
|
354
359
|
_PUBLISH_EVENT_CALL_RE = re.compile(r'\.publishEvent\s*\(')
|
|
355
|
-
_NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*[\({]')
|
|
360
|
+
_NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*' + _GENERIC_ARGS + r'\s*[\({]')
|
|
356
361
|
|
|
357
362
|
# Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
|
|
358
363
|
_FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
|
|
@@ -1122,6 +1127,102 @@ def _build_same_package_map(symbols: list[SymbolRecord]) -> dict[str, dict[str,
|
|
|
1122
1127
|
return result
|
|
1123
1128
|
|
|
1124
1129
|
|
|
1130
|
+
# Reserved words that read like calls (`if (`, `for (`, …). Can never be sibling
|
|
1131
|
+
# method names (Java reserves them), but guard the intra-class scan anyway.
|
|
1132
|
+
_CALL_KEYWORDS: frozenset[str] = frozenset({
|
|
1133
|
+
"if", "for", "while", "switch", "catch", "return", "synchronized",
|
|
1134
|
+
"new", "super", "this", "assert",
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _method_body(raw_lines: list[str], start_idx: int) -> str:
|
|
1139
|
+
"""Source text of the method/constructor declared at raw_lines[start_idx],
|
|
1140
|
+
from its first '{' to the matching '}' (brace-matched, string/char aware).
|
|
1141
|
+
|
|
1142
|
+
The signature prefix before '{' is dropped so the method's own name is not
|
|
1143
|
+
scanned as a call site. Returns "" for a bodyless declaration (abstract /
|
|
1144
|
+
interface method terminated by ';' before any '{').
|
|
1145
|
+
"""
|
|
1146
|
+
depth = 0
|
|
1147
|
+
started = False
|
|
1148
|
+
out: list[str] = []
|
|
1149
|
+
for i in range(start_idx, len(raw_lines)):
|
|
1150
|
+
line = raw_lines[i]
|
|
1151
|
+
if not started:
|
|
1152
|
+
if "{" in line:
|
|
1153
|
+
started = True
|
|
1154
|
+
line = line[line.index("{"):]
|
|
1155
|
+
elif ";" in line:
|
|
1156
|
+
return "" # bodyless declaration
|
|
1157
|
+
else:
|
|
1158
|
+
continue # multi-line signature continuation
|
|
1159
|
+
out.append(line)
|
|
1160
|
+
depth += _count_net_braces(line)
|
|
1161
|
+
if depth <= 0:
|
|
1162
|
+
break
|
|
1163
|
+
return "\n".join(out)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _intra_class_call_edges(symbols: list[SymbolRecord], source: str) -> list[RelationEdge]:
|
|
1167
|
+
"""Method-level `calls` edges for intra-class invocations.
|
|
1168
|
+
|
|
1169
|
+
`discontinueOrder(){ stopOrder(...); }` →
|
|
1170
|
+
OrderServiceImpl#discontinueOrder --calls--> OrderServiceImpl#stopOrder.
|
|
1171
|
+
|
|
1172
|
+
Class-level call scans miss method-to-method calls within a single class, so
|
|
1173
|
+
impact-chain on a private/helper method (e.g. OrderServiceImpl#stopOrder) found
|
|
1174
|
+
zero method-level callers and degraded to a wrong class-level expansion. Resolves
|
|
1175
|
+
bare `m(...)` and `this.m(...)` calls whose target is a sibling METHOD of the same
|
|
1176
|
+
class. Overloads link to all same-name siblings (arity not resolved by regex).
|
|
1177
|
+
Deterministic, in-source only — no cross-file or runtime inference.
|
|
1178
|
+
"""
|
|
1179
|
+
callers = [s for s in symbols if s.symbol_kind in ("method", "constructor") and s.line]
|
|
1180
|
+
if not callers:
|
|
1181
|
+
return []
|
|
1182
|
+
|
|
1183
|
+
# Per-class sibling index: simple method name → [method FQNs]. Constructors are
|
|
1184
|
+
# not call targets (a `new X(...)` site is a different relation).
|
|
1185
|
+
siblings: dict[str, dict[str, list[str]]] = {}
|
|
1186
|
+
for s in symbols:
|
|
1187
|
+
if s.symbol_kind == "method" and "#" in s.symbol:
|
|
1188
|
+
cls = _enclosing_class(s.symbol)
|
|
1189
|
+
name = s.symbol.rsplit("#", 1)[1]
|
|
1190
|
+
siblings.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
|
|
1191
|
+
if not siblings:
|
|
1192
|
+
return []
|
|
1193
|
+
|
|
1194
|
+
raw_lines = source.splitlines()
|
|
1195
|
+
edges: list[RelationEdge] = []
|
|
1196
|
+
for caller in callers:
|
|
1197
|
+
cls = _enclosing_class(caller.symbol)
|
|
1198
|
+
sib = siblings.get(cls)
|
|
1199
|
+
if not sib:
|
|
1200
|
+
continue
|
|
1201
|
+
body = _method_body(raw_lines, caller.line - 1)
|
|
1202
|
+
if not body:
|
|
1203
|
+
continue
|
|
1204
|
+
body = _STRING_LITERAL_RE.sub('', _strip_java_comments(body))
|
|
1205
|
+
for name, fqns in sib.items():
|
|
1206
|
+
if name in _CALL_KEYWORDS:
|
|
1207
|
+
continue
|
|
1208
|
+
# bare `name(` (not preceded by word char or '.') OR explicit `this.name(`
|
|
1209
|
+
pat = (r'(?<![\w.])' + re.escape(name) + r'\s*\('
|
|
1210
|
+
+ r'|\bthis\s*\.\s*' + re.escape(name) + r'\s*\(')
|
|
1211
|
+
if not re.search(pat, body):
|
|
1212
|
+
continue
|
|
1213
|
+
for tgt in fqns:
|
|
1214
|
+
if tgt == caller.symbol:
|
|
1215
|
+
continue # skip self-recursion self-loop
|
|
1216
|
+
edges.append(RelationEdge(
|
|
1217
|
+
from_symbol=caller.symbol,
|
|
1218
|
+
to_symbol=tgt,
|
|
1219
|
+
type="calls",
|
|
1220
|
+
confidence="medium",
|
|
1221
|
+
evidence={"type": "method_call", "value": f"{name}(...)"},
|
|
1222
|
+
))
|
|
1223
|
+
return edges
|
|
1224
|
+
|
|
1225
|
+
|
|
1125
1226
|
def _build_relations(
|
|
1126
1227
|
symbols: list[SymbolRecord],
|
|
1127
1228
|
raw_imports: list[str],
|
|
@@ -1557,6 +1658,9 @@ def _build_relations(
|
|
|
1557
1658
|
evidence={"type": "method_call", "value": f"{_tgt.split('.')[-1]}.…(…)"},
|
|
1558
1659
|
))
|
|
1559
1660
|
|
|
1661
|
+
# ── Intra-class method calls: EnclosingMethod -> calls -> SameClass#sibling ──
|
|
1662
|
+
edges.extend(_intra_class_call_edges(symbols, source))
|
|
1663
|
+
|
|
1560
1664
|
seen: set[tuple[str, str, str]] = set()
|
|
1561
1665
|
unique: list[RelationEdge] = []
|
|
1562
1666
|
for e in edges:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
"""sourcecode telemetry —
|
|
1
|
+
"""sourcecode telemetry — anonymous usage metrics, on by default (opt-out).
|
|
2
2
|
|
|
3
3
|
Public API:
|
|
4
4
|
is_enabled() → bool
|
|
5
5
|
record(event, **kw) → None (fire-and-forget)
|
|
6
6
|
session_id() → str (ephemeral 8-char hex, new each process)
|
|
7
7
|
|
|
8
|
-
Telemetry is
|
|
9
|
-
|
|
8
|
+
Telemetry is enabled by default and stays anonymous. It can be disabled at any
|
|
9
|
+
time via `sourcecode telemetry disable`, SOURCECODE_TELEMETRY=0, or DO_NOT_TRACK=1.
|
|
10
10
|
|
|
11
11
|
Nothing sensitive (code, paths, secrets, output) is ever collected.
|
|
12
12
|
See docs/privacy.md for full details.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""Persistent telemetry configuration.
|
|
2
2
|
|
|
3
|
+
Telemetry is enabled by default (opt-out). It stays anonymous and never
|
|
4
|
+
collects source code, paths, secrets or repository content.
|
|
5
|
+
|
|
3
6
|
Config file: ~/.config/sourcecode/config.json
|
|
7
|
+
Disable: `sourcecode telemetry disable`, SOURCECODE_TELEMETRY=0, or DO_NOT_TRACK=1
|
|
4
8
|
Env override: SOURCECODE_TELEMETRY=0 (disable) or =1 (enable)
|
|
5
9
|
"""
|
|
6
10
|
|
|
@@ -14,6 +18,18 @@ from typing import Any
|
|
|
14
18
|
_ENV_VAR = "SOURCECODE_TELEMETRY"
|
|
15
19
|
_CONFIG_FILE = Path.home() / ".config" / "sourcecode" / "config.json"
|
|
16
20
|
|
|
21
|
+
# CI markers — when no explicit choice has been made, telemetry defaults OFF
|
|
22
|
+
# in CI (no human to see the first-run notice), ON otherwise.
|
|
23
|
+
_CI_VARS = (
|
|
24
|
+
"CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "CIRCLECI",
|
|
25
|
+
"TRAVIS", "JENKINS_URL", "BUILDKITE", "GITLAB_CI", "TF_BUILD",
|
|
26
|
+
"TEAMCITY_VERSION", "DRONE", "SEMAPHORE",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _in_ci() -> bool:
|
|
31
|
+
return any(os.environ.get(v) for v in _CI_VARS)
|
|
32
|
+
|
|
17
33
|
|
|
18
34
|
def _load() -> dict[str, Any]:
|
|
19
35
|
try:
|
|
@@ -31,17 +47,27 @@ def _save(data: dict[str, Any]) -> None:
|
|
|
31
47
|
|
|
32
48
|
|
|
33
49
|
def is_enabled() -> bool:
|
|
34
|
-
"""True
|
|
50
|
+
"""True unless telemetry has been explicitly disabled.
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
Telemetry is enabled by default (opt-out). Precedence, highest first:
|
|
53
|
+
1. SOURCECODE_TELEMETRY env var (0 = off, 1 = on)
|
|
54
|
+
2. DO_NOT_TRACK env var (any value other than ""/"0" turns it off)
|
|
55
|
+
3. config file 'enabled' flag, if the user has made an explicit choice
|
|
56
|
+
4. default: True — except in CI, where it defaults to False
|
|
38
57
|
"""
|
|
39
58
|
env = os.environ.get(_ENV_VAR, "").strip()
|
|
40
59
|
if env == "0":
|
|
41
60
|
return False
|
|
42
61
|
if env == "1":
|
|
43
62
|
return True
|
|
44
|
-
|
|
63
|
+
dnt = os.environ.get("DO_NOT_TRACK", "").strip()
|
|
64
|
+
if dnt not in ("", "0"):
|
|
65
|
+
return False
|
|
66
|
+
stored = _load().get("telemetry", {}).get("enabled")
|
|
67
|
+
if stored is not None:
|
|
68
|
+
return bool(stored)
|
|
69
|
+
# No explicit choice yet: on by default, off under CI.
|
|
70
|
+
return not _in_ci()
|
|
45
71
|
|
|
46
72
|
|
|
47
73
|
def has_been_asked() -> bool:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""First-run
|
|
1
|
+
"""First-run telemetry notice.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Telemetry is enabled by default (opt-out) and stays anonymous. The notice is
|
|
4
|
+
shown exactly once, only on interactive TTYs, to inform the user that
|
|
5
|
+
telemetry is on and how to turn it off. It does not ask a question — it
|
|
6
|
+
informs and respects the user's right to disable.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
Notice is written to stderr so it doesn't pollute stdout output.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
@@ -11,11 +13,11 @@ from __future__ import annotations
|
|
|
11
13
|
import os
|
|
12
14
|
import sys
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
_NOTICE = """\
|
|
15
17
|
\033[2m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
16
|
-
sourcecode —
|
|
18
|
+
sourcecode — anonymous telemetry is ON by default
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Anonymous usage metrics help improve sourcecode.
|
|
19
21
|
|
|
20
22
|
Collected: tool version, Python version, OS, commands used,
|
|
21
23
|
flags used, approximate repo size, execution duration, errors.
|
|
@@ -23,9 +25,9 @@ _PROMPT = """\
|
|
|
23
25
|
Never collected: source code, file paths, file names, secrets,
|
|
24
26
|
tokens, environment variables, or any repository content.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
Disable at any time:
|
|
27
29
|
sourcecode telemetry disable
|
|
28
|
-
export SOURCECODE_TELEMETRY=0
|
|
30
|
+
export SOURCECODE_TELEMETRY=0 (or DO_NOT_TRACK=1)
|
|
29
31
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m
|
|
30
32
|
"""
|
|
31
33
|
|
|
@@ -43,21 +45,16 @@ def _is_interactive() -> bool:
|
|
|
43
45
|
return not any(os.environ.get(v) for v in ci_vars)
|
|
44
46
|
|
|
45
47
|
|
|
46
|
-
def
|
|
47
|
-
"""
|
|
48
|
+
def show_first_run_notice() -> None:
|
|
49
|
+
"""Print the one-time telemetry notice on interactive terminals.
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
Never raises. Does nothing on non-interactive / CI environments.
|
|
52
|
+
Telemetry stays enabled regardless — this only informs the user.
|
|
51
53
|
"""
|
|
52
54
|
if not _is_interactive():
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
+
return
|
|
55
56
|
try:
|
|
56
|
-
sys.stderr.write(
|
|
57
|
-
sys.stderr.write(" Enable anonymous telemetry? [y/N]: ")
|
|
57
|
+
sys.stderr.write(_NOTICE)
|
|
58
58
|
sys.stderr.flush()
|
|
59
|
-
answer = sys.stdin.readline().strip().lower()
|
|
60
|
-
sys.stderr.write("\n")
|
|
61
|
-
return answer == "y"
|
|
62
59
|
except Exception:
|
|
63
|
-
|
|
60
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|