sourcecode 5.8.12__py3-none-any.whl → 5.8.13__py3-none-any.whl

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.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "5.8.12"
7
+ __version__ = "5.8.13"
@@ -16,7 +16,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
16
16
 
17
17
  ## Current Synchronization
18
18
 
19
- **As of 2026-08-19, the external `5.8.11` re-audit:** this is the current status of
19
+ **As of 2026-08-20, release `5.8.13` and the external `5.8.11` re-audit:** this is the current status of
20
20
  the 5.8.8/5.8.9 audit queue. The subject remained `saint-server` at `3dde0376`, with a
21
21
  byte-identical CIR and the same 53/61 invocables exercised. The score is **78/100**
22
22
  (75 in 5.8.8, 80 in 5.8.9). The observations below remain historical evidence; only
@@ -30,6 +30,7 @@ this table declares present state.
30
30
  | `AUD-588-B05` | **closed** | `c15f5b6`; changed container-wired components block a false `unaffected` disposition. |
31
31
  | `AUD-511-R03` / `AUD-588-B07` | **implemented in 5.8.12** | `47aee1e` gives the published `OUTPUT_TOO_LARGE` envelope precedence over an inconsistent process status and preserves the excerpt in every branch. |
32
32
  | `AUD-588-B08` | **closed** | `c084555`; `impact` publishes its independent stdout-byte budget. |
33
+ | `MCP-001` | **closed 5.8.13** | Deep corpus testing reproduced concurrent `CliRunner` stream capture leaking a tool response to the host stdout. `b2c9660` serializes the narrow process-global capture seam; concurrent real MCP calls and a regression test verify response isolation. |
33
34
  | `AUD-588-B11` (container/units subset) | **partially implemented in 5.8.12** | `d745d31` discovers nested Maven modules; `273ae8e` suppresses unsupported `pr-impact.unaffected_basis`. `impact` naming, source population units, risk tiers and plan narrative remain open. |
34
35
  | `AUD-511-R02` / `AUD-589-B02` | **partially mitigated in 5.8.12, still under investigation** | `b93f230` reuses one `SpringSemanticModel` across every declared `data-exposure` seed; `473f47a` routes `validation` through the shared CIR cache, and `91bd0c0` retains subcommand `--no-cache` compatibility. On the local BroadleafCommerce corpus at the final analyzer fingerprint, a first shared-CIR fill took 10.46 s and its warm `validation --compact` hit took 4.01 s with byte-identical output. This is a same-host cache-state observation, not the 5.8.9/5.8.11 external A/B: the six-run C3-120 comparison and the configured `saint-server` reproduction remain required. |
35
36
  | `AUD-588-B02`, `B06`, B11 residuals, `B12` | **partially open in 5.8.12** | `3869c87` excludes explicit EclipseLink from Hibernate applicability and effort; `037223e` publishes provider non-coverage. B11 parity and controlled cost calibration remain required. |
@@ -594,7 +595,7 @@ class's subject, not its provenance.
594
595
 
595
596
  | ID | Item | Found | Severity | Status |
596
597
  |---|---|---|---|---|
597
- | P-1 | Telemetry is **opt-out**. Auditing a public-sector health client's code, it must be disabled *before* the first run, not after | 3.2.0 | **High (procurement)** | **closed 3.3.0** — opt-in: with no explicit choice on record, `is_enabled()` is False everywhere, so a fresh install transmits nothing on the run that prints the notice. The CI special case is gone with the default it existed to override; CI is still detected for one purpose, skipping a notice that has no terminal to appear on. The first-run notice became an invitation that states what *would* be collected. `telemetry status` distinguishes "no choice recorded" from "you chose disabled" (`stored_choice()` returns `None`, not `False`) — a buyer auditing this needs to know which one they have. An explicit choice made before the change is honoured unchanged |
598
+ | P-1 | Telemetry default and procurement disclosure | 3.2.0 | **High (procurement)** | **reopened and corrected 5.8.12** — telemetry is on by default by explicit product decision, with `ask telemetry disable`, `SOURCECODE_TELEMETRY=0` and `DO_NOT_TRACK=1` honored before any event is sent. The first-run notice discloses the fields and the disable path. `telemetry status` distinguishes the default from an explicit disabled choice; the privacy policy documents the endpoint, 90-day retention and bounded anonymous fields. This supersedes the historical 3.3.0 opt-in closure for the current release |
598
599
  | P-2 | Licence state is ambiguous: `auth status` → `{"status":"unauthenticated","pro":true,"pro_reason":"early-adoption unlock"}`. The buyer cannot tell what it will cost or what they lose when the door closes | 3.2.0 | Medium | **closed 3.4.0** — one authority, `license.entitlement()`: `entitlement` (what runs), `source` (why: `license_key` / `early_adoption_unlock` / `free_tier`), `authenticated` (a separate fact — today one can be entitled without a credential), `paywall_active`, and `when_it_changes`, which states what the user loses when that source stops applying, in the terms the gate uses and **never as a price**. `is_pro` — what every gate reads — is now derived from it rather than computed beside it, which is how the status page came to contradict the commands. The legacy keys stay for consumers, are derived from the authority, and are listed in `deprecated_fields` |
599
600
 
600
601
  | P-3 | **The price is not published.** With P-2 closed the entitlement is unambiguous, but a buyer still cannot see what Pro costs or what a team of 25 pays. Eval #5 lists *"precio Pro público"* among the conditions for the top of its price band, beside the robustness fixes | 3.2.1 (eval #5) | Medium (procurement) | open — packaging question, not a page: the field's own reading is that **the value is frontal** (a first audit replaces 5–15 person-days) **and the recurring half is the CI gate**, so a full seat subscription on a product whose value peaks on run one *"genera churn en el mes 4"*. Model to test: one-off assessment **3–5 k€ per repo** + **8–12 €/dev/month** for the gate. Depends on C3-22: without the gate there is nothing recurring to sell |
@@ -138,7 +138,7 @@ pipx install sourcecode # isolated install, no venv needed
138
138
 
139
139
  # Verify
140
140
  ask version
141
- # ask 5.8.12
141
+ # ask 5.8.13
142
142
  ```
143
143
 
144
144
  Requires Python 3.9+.
@@ -1199,7 +1199,7 @@ explaining. The rest do one thing; `ask <command> --help` is the whole story.
1199
1199
  | `ask retrieve` | parked | Typed knowledge queries over the model — kept working, no longer developed |
1200
1200
  | `ask baseline capture\|diff\|trend` | supported | Versioned architectural metrics over time; see [`ask trend`](#ask-trend-dir--how-the-architecture-moved) |
1201
1201
  | `ask cache status\|warm\|model\|clear\|freshness` | supported | Cache inspection; `ask cache model` states what a warm buys each command |
1202
- | `ask config` · `ask version` · `ask activate` · `ask auth` · `ask telemetry` · `ask mcp` | supported | Configuration, version, licence, authentication, telemetry (off by default), MCP integration |
1202
+ | `ask config` · `ask version` · `ask activate` · `ask auth` · `ask telemetry` · `ask mcp` | supported | Configuration, version, licence, authentication, telemetry (on by default), MCP integration |
1203
1203
 
1204
1204
  ## Typical workflows
1205
1205
 
@@ -1287,7 +1287,7 @@ ask mcp serve
1287
1287
  ask mcp remove
1288
1288
  ```
1289
1289
 
1290
- The MCP server exposes structural analysis tools to AI agents without requiring the agent to call the CLI directly. Claude Desktop and Cursor can query impact, endpoints, and context through the MCP protocol.
1290
+ The MCP server exposes structural analysis tools to AI agents without requiring the agent to call the CLI directly. Claude Desktop and Cursor can query impact, endpoints, and context through the MCP protocol. Concurrent requests are accepted safely: ASK serializes only the in-process CLI capture seam, because its stdout/stderr redirection is process-global, so one tool response cannot contaminate another.
1291
1291
 
1292
1292
  ---
1293
1293
 
sourcecode/cli.py CHANGED
@@ -3033,13 +3033,59 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
3033
3033
 
3034
3034
  _orig_cmd_main = cmd.main
3035
3035
 
3036
+ _TELEMETRY_COMMANDS = frozenset({
3037
+ "repo-ir", "impact", "endpoints", "export", "validation", "delta", "contract-diff",
3038
+ "plan", "compare", "trend", "timeline", "spring-audit", "verify-edit", "verify", "risk",
3039
+ "enrich", "audit-report", "migrate-recipe", "data-exposure", "posture", "migrate-check",
3040
+ "impact-chain", "pr-impact", "explain", "onboard", "review-pr", "fix-bug", "modernize",
3041
+ "rename-class", "chunk-file", "activate", "selftest", "explain-endpoint", "regress", "version",
3042
+ "schema", "config", "archetype", "cold-start", "telemetry", "mcp", "cache", "baseline",
3043
+ "auth", "retrieve", "prepare-context",
3044
+ })
3045
+
3046
+ def _telemetry_command(args_for_command: Optional[list[str]]) -> str:
3047
+ """Extract only a registered CLI command, never a path or option value."""
3048
+ tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
3049
+ for token in tokens:
3050
+ if token in _TELEMETRY_COMMANDS:
3051
+ return token
3052
+ return "analyze"
3053
+
3054
+ def _telemetry_flags(args_for_command: Optional[list[str]]) -> list[str]:
3055
+ tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
3056
+ try:
3057
+ from sourcecode.telemetry.filters import _SAFE_FLAGS
3058
+ return sorted({token for token in tokens if token in _SAFE_FLAGS})
3059
+ except Exception:
3060
+ return []
3061
+
3036
3062
  def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
3037
3063
  if args is not None:
3038
3064
  # CliRunner / programmatic call: preprocess the explicit args list.
3039
3065
  _set_detected_path(".")
3040
3066
  args = _preprocess_args(list(args))
3041
3067
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
3042
- return _orig_cmd_main(args=args, **kwargs)
3068
+ started = time.monotonic()
3069
+ command = _telemetry_command(args)
3070
+ success = True
3071
+ try:
3072
+ return _orig_cmd_main(args=args, **kwargs)
3073
+ except BaseException:
3074
+ success = False
3075
+ raise
3076
+ finally:
3077
+ try:
3078
+ from sourcecode import telemetry as _tel
3079
+ _tel.record(
3080
+ "execution_completed",
3081
+ cmd=("mcp" if command == "mcp" else "telemetry" if command == "telemetry" else "analyze"),
3082
+ command=command,
3083
+ flags=_telemetry_flags(args),
3084
+ duration_s=time.monotonic() - started,
3085
+ success=success,
3086
+ )
3087
+ except Exception:
3088
+ pass
3043
3089
 
3044
3090
  cmd.main = _cmd_main
3045
3091
  return cmd
@@ -3055,7 +3101,7 @@ try:
3055
3101
  except Exception:
3056
3102
  pass
3057
3103
 
3058
- telemetry_app = typer.Typer(help="Manage anonymous telemetry (off by default; opt-in).", rich_markup_mode="rich")
3104
+ telemetry_app = typer.Typer(help="Manage anonymous telemetry (on by default; disable any time).", rich_markup_mode="rich")
3059
3105
  app.add_typer(telemetry_app, name="telemetry")
3060
3106
 
3061
3107
  mcp_app = typer.Typer(help="MCP integration: setup, status, serve, remove.", rich_markup_mode="rich")
@@ -3083,9 +3129,8 @@ app.add_typer(retrieve_app, name="retrieve")
3083
3129
  def _maybe_show_telemetry_notice() -> None:
3084
3130
  """Show first-run telemetry notice once, on interactive TTYs only.
3085
3131
 
3086
- Telemetry is off by default (opt-in). The notice is an invitation, not a
3087
- disclosure: nothing has been collected when it appears. Marked as shown so it
3088
- appears only once.
3132
+ Telemetry is on by default. The notice is a one-time disclosure of what is
3133
+ collected and how to disable it.
3089
3134
  """
3090
3135
  try:
3091
3136
  from sourcecode.telemetry.config import has_been_asked, mark_asked
@@ -5314,26 +5359,7 @@ def main(
5314
5359
  perf.stop("serialize", _perf_serialize)
5315
5360
  perf.flush_recorder()
5316
5361
 
5317
- # 5. Telemetry (fire-and-forget, never blocks)
5318
- try:
5319
- from sourcecode import telemetry as _tel
5320
- _tel.record(
5321
- "execution_completed",
5322
- cmd="analyze",
5323
- flags=_active_flags(
5324
- dependencies, graph_modules, docs, full_metrics,
5325
- semantics, architecture, git_context, env_map,
5326
- code_notes, agent, compact, tree, no_redact, format,
5327
- ),
5328
- output_fmt=format,
5329
- file_count=len(sm.file_paths),
5330
- duration_s=time.monotonic() - _t0,
5331
- success=True,
5332
- )
5333
- except Exception:
5334
- pass
5335
-
5336
- # 6. Write output (CLI-04)
5362
+ # 5. Write output (CLI-04)
5337
5363
  _progress.finish()
5338
5364
  if format == "json":
5339
5365
  _uncommitted_fresh, _uncommitted_fresh_basis = _uncommitted_fact(
@@ -5926,17 +5952,6 @@ def prepare_context_cmd(
5926
5952
  except Exception:
5927
5953
  pass # not JSON (or unreadable) → serve exactly what was stored
5928
5954
  _emit_command_output(_cached_pctx, output_path, copy)
5929
- try:
5930
- from sourcecode import telemetry as _tel
5931
- _tel.record(
5932
- "execution_completed",
5933
- cmd="prepare-context",
5934
- feature=task,
5935
- output_fmt=format,
5936
- duration_s=0.0,
5937
- )
5938
- except Exception:
5939
- pass
5940
5955
  return
5941
5956
 
5942
5957
  _scope_files = None
@@ -6463,18 +6478,6 @@ def prepare_context_cmd(
6463
6478
  fmt=("yaml" if format == "yaml" else "json")
6464
6479
  )
6465
6480
 
6466
- try:
6467
- from sourcecode import telemetry as _tel
6468
- _tel.record(
6469
- "execution_completed",
6470
- cmd="prepare-context",
6471
- feature=task,
6472
- output_fmt=format,
6473
- duration_s=_time.perf_counter() - _t0,
6474
- )
6475
- except Exception:
6476
- pass
6477
-
6478
6481
  from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
6479
6482
  _nudge()
6480
6483
 
@@ -6491,11 +6494,11 @@ def telemetry_status(
6491
6494
  enabled = is_enabled()
6492
6495
  choice = stored_choice()
6493
6496
  status = "enabled" if enabled else "disabled"
6494
- lines = [f"Telemetry: {status} (off by default; opt-in)"]
6495
- # "off because you said so" and "off because nobody asked you" are different
6496
- # answers, and a buyer auditing this needs to be told which one they have.
6497
+ lines = [f"Telemetry: {status} (on by default; disable with `ask telemetry disable`)"]
6498
+ # The default and an explicit disabled choice are different answers, and a
6499
+ # buyer auditing this needs to be told which one they have.
6497
6500
  if choice is None:
6498
- lines.append(" No choice recorded — nothing has been collected or sent.")
6501
+ lines.append(" No choice recorded — the on-by-default setting applies.")
6499
6502
  else:
6500
6503
  lines.append(f" Your recorded choice: {'enabled' if choice else 'disabled'}.")
6501
6504
  lines.append(f" Config: {config_file_path()}")
@@ -6508,7 +6511,7 @@ def telemetry_status(
6508
6511
 
6509
6512
  @telemetry_app.command("enable")
6510
6513
  def telemetry_enable() -> None:
6511
- """Opt in to anonymous telemetry."""
6514
+ """Enable anonymous telemetry and remember the choice."""
6512
6515
  from sourcecode.telemetry.config import set_enabled
6513
6516
  from sourcecode import telemetry as _tel
6514
6517
  set_enabled(True)
@@ -6521,11 +6524,11 @@ def telemetry_enable() -> None:
6521
6524
 
6522
6525
  @telemetry_app.command("disable")
6523
6526
  def telemetry_disable() -> None:
6524
- """Opt out of anonymous telemetry."""
6527
+ """Disable anonymous telemetry and remember the choice."""
6525
6528
  from sourcecode.telemetry.config import set_enabled
6526
6529
  set_enabled(False)
6527
6530
  typer.echo("Telemetry disabled. No data will be collected or sent.")
6528
- typer.echo("Telemetry is off by default; this choice is recorded so the notice stops asking.")
6531
+ typer.echo("Telemetry is enabled by default; this choice is recorded so the notice stops appearing.")
6529
6532
  typer.echo("Re-enable at any time: ask telemetry enable")
6530
6533
 
6531
6534
 
@@ -14190,7 +14193,7 @@ def config_cmd(
14190
14193
  _answer = _TextAnswer(output_path)
14191
14194
  _answer.say(f"ask {__version__}")
14192
14195
  _answer.say(f"Config: {config_file_path()}")
14193
- _answer.say(f"Telemetry: {'enabled' if is_enabled() else 'disabled'} (off by default; opt-in)")
14196
+ _answer.say(f"Telemetry: {'enabled' if is_enabled() else 'disabled'} (on by default; disable with `ask telemetry disable`)")
14194
14197
  _answer.say("")
14195
14198
 
14196
14199
  # F-AX: the declaration may sit outside the analysed tree, and *which* file
sourcecode/mcp/runner.py CHANGED
@@ -7,11 +7,15 @@ lookup, no process fork, no stdout encoding issues.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
+ import threading
10
11
  from typing import Any
11
12
 
12
13
  from typer.testing import CliRunner
13
14
 
14
15
  _runner = CliRunner()
16
+ # CliRunner temporarily replaces process-global stdout/stderr. MCP dispatches
17
+ # tools concurrently, so every in-process invocation must share this lock.
18
+ _runner_lock = threading.RLock()
15
19
 
16
20
 
17
21
  class CommandError(RuntimeError):
@@ -44,7 +48,8 @@ def run_command(args: list[str]) -> Any:
44
48
  # Pass raw args to invoke — the _cmd_main hook inside cli.py handles path
45
49
  # extraction via _preprocess_args. Pre-processing here would strip the path
46
50
  # from args, then _cmd_main would re-process the stripped list and lose it.
47
- result = _runner.invoke(app, list(args))
51
+ with _runner_lock:
52
+ result = _runner.invoke(app, list(args))
48
53
 
49
54
  if result.exit_code != 0:
50
55
  stdout_raw = getattr(result, "output", "")
sourcecode/mcp/server.py CHANGED
@@ -51,9 +51,8 @@ def _record_tool_invocation(name: Any, success: bool, started: float) -> None:
51
51
 
52
52
  Aggregate only: which of our own tools ran, whether it succeeded, and a
53
53
  duration bucket. Never the arguments — those carry repository paths — and
54
- never any result content. Honours the same opt-in as every other event
55
- (off until `ask telemetry enable` or SOURCECODE_TELEMETRY=1); when
56
- telemetry is off, `record` returns before building anything.
54
+ never any result content. Honours the same default-on setting as every other
55
+ event; `ask telemetry disable` or the documented environment controls stop it.
57
56
  """
58
57
  try:
59
58
  import time as _time
@@ -1480,7 +1479,7 @@ def telemetry(action: str) -> dict:
1480
1479
  """Manage telemetry settings.
1481
1480
 
1482
1481
  Maps to: ask telemetry <action>
1483
- action: one of "status" (show current state), "enable" (opt in), "disable" (opt out).
1482
+ action: one of "status" (show current state), "enable", or "disable".
1484
1483
  Valid values: "status" | "enable" | "disable"
1485
1484
  """
1486
1485
  # FIX-P2-10: enumerate valid actions in docstring so agents don't guess.
@@ -27,7 +27,7 @@ from typing import Any, Optional
27
27
  #: The date `__version__` was released, from that version's CHANGELOG entry.
28
28
  #: `tests/test_release_info.py` fails the build if the two disagree, so this is
29
29
  #: a copy of a fact rather than a second authority for it.
30
- RELEASE_DATE = "2026-08-19"
30
+ RELEASE_DATE = "2026-08-20"
31
31
 
32
32
  #: Releases per day, measured over the 30 releases before this one (the same
33
33
  #: computation the test re-runs against the CHANGELOG). Stable across window
@@ -1,13 +1,12 @@
1
- """sourcecode telemetry — anonymous usage metrics, off until you turn them on (opt-in).
1
+ """sourcecode telemetry — anonymous usage metrics, on by default.
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 opt-in and stays anonymous: nothing is collected until `ask telemetry
9
- enable` (or SOURCECODE_TELEMETRY=1). It was opt-out until 3.3.0 — see config.py for
10
- why the default moved.
8
+ Telemetry is enabled by default and stays anonymous. Disable it with `ask telemetry
9
+ disable`, SOURCECODE_TELEMETRY=0, or DO_NOT_TRACK=1.
11
10
 
12
11
  Nothing sensitive (code, paths, secrets, output) is ever collected.
13
12
  See docs/privacy.md for full details.
@@ -72,6 +71,7 @@ def record(
72
71
  event: str,
73
72
  *,
74
73
  cmd: str = "analyze",
74
+ command: Optional[str] = None,
75
75
  flags: Optional[list[str]] = None,
76
76
  output_fmt: str = "json",
77
77
  file_count: Optional[int] = None,
@@ -98,6 +98,7 @@ def record(
98
98
  os=_platform_os(),
99
99
  arch=_platform_arch(),
100
100
  cmd=cmd,
101
+ command=command,
101
102
  flags=flags or [],
102
103
  output_fmt=output_fmt,
103
104
  repo_size=(
@@ -1,13 +1,12 @@
1
1
  """Persistent telemetry configuration.
2
2
 
3
- Telemetry is **off until the user turns it on** (opt-in). It stays anonymous and
4
- never collects source code, paths, secrets or repository content.
3
+ Telemetry is **on by default** and stays anonymous. Users can disable it with
4
+ ``ask telemetry disable``, ``SOURCECODE_TELEMETRY=0`` or ``DO_NOT_TRACK=1``;
5
+ it never collects source code, paths, secrets or repository content.
5
6
 
6
- It was opt-out until 3.3.0. Field evaluation #3 was the first audit of regulated
7
- third-party code — public-sector health data — and there the default is not a
8
- preference but a procurement blocker: the reviewer had to disable telemetry
9
- *before the first run*, which the product gave them no way to do. Nothing is
10
- collected before an explicit choice, so there is nothing to disable in time.
7
+ The default is explicit and reversible: users can disable telemetry before a run
8
+ with `SOURCECODE_TELEMETRY=0` or `DO_NOT_TRACK=1`, or persist that choice with
9
+ `ask telemetry disable`.
11
10
 
12
11
  Config file: ~/.config/sourcecode/config.json
13
12
  Enable: `ask telemetry enable` or SOURCECODE_TELEMETRY=1
@@ -43,26 +42,22 @@ def _save(data: dict[str, Any]) -> None:
43
42
  def stored_choice() -> "bool | None":
44
43
  """The explicit choice on record, or None when the user has made none.
45
44
 
46
- `None` is a state, not a synonym for `False`: "off because nobody has been
47
- asked yet" and "off because you turned it off" are different answers, and
48
- `telemetry status` prints them differently.
45
+ `None` is a state, not a synonym for `False`: "on because the default applies"
46
+ and "off because you turned it off" are different answers, and `telemetry
47
+ status` prints them differently.
49
48
  """
50
49
  stored = _load().get("telemetry", {}).get("enabled")
51
50
  return None if stored is None else bool(stored)
52
51
 
53
52
 
54
53
  def is_enabled() -> bool:
55
- """True only when telemetry has been explicitly turned on.
54
+ """Return whether telemetry is enabled. It defaults to enabled.
56
55
 
57
- Telemetry is **opt-in**: no choice on record means off, everywhere, CI or
58
- not. Precedence, highest first:
56
+ Precedence, highest first:
59
57
  1. SOURCECODE_TELEMETRY env var (0 = off, 1 = on)
60
58
  2. DO_NOT_TRACK env var (any value other than ""/"0" turns it off)
61
59
  3. config file 'enabled' flag, if the user has made an explicit choice
62
- 4. default: False
63
-
64
- There is no CI special case any more, because there is nothing left for it
65
- to protect against — the default it used to override is now off.
60
+ 4. default: True
66
61
  """
67
62
  env = os.environ.get(_ENV_VAR, "").strip()
68
63
  if env == "0":
@@ -75,7 +70,7 @@ def is_enabled() -> bool:
75
70
  stored = stored_choice()
76
71
  if stored is not None:
77
72
  return stored
78
- return False
73
+ return True
79
74
 
80
75
 
81
76
  def has_been_asked() -> bool:
@@ -100,7 +95,7 @@ def mark_asked() -> None:
100
95
  def get_install_id() -> str:
101
96
  """Stable anonymous install id — a random UUID v4.
102
97
 
103
- Created lazily on first opted-in event. NOT derived from hardware, email,
98
+ Created lazily on first enabled event. NOT derived from hardware, email,
104
99
  hostname or any identifier — it only says "the same install across runs",
105
100
  which is what enables unique-user, conversion and retention metrics.
106
101
  Returns "" if it cannot be persisted (telemetry then degrades to events
@@ -1,13 +1,10 @@
1
1
  """First-run telemetry notice.
2
2
 
3
- Telemetry is **opt-in**: nothing has been collected when this notice appears, and
4
- nothing will be until the user runs `ask telemetry enable`. The notice is shown
5
- exactly once, only on interactive TTYs, and it is an invitation — it states what
6
- *would* be collected if they said yes, and it never changes the setting.
3
+ Telemetry is **on by default**. The notice is shown exactly once, only on
4
+ interactive TTYs, and states what is collected and how to disable it.
7
5
 
8
- The invitation is deliberately printed even though the answer is already "no":
9
- a user who never learns the option exists cannot choose to help, and a silent
10
- collector is the thing this release removed.
6
+ The disclosure is deliberately printed once so users know what is collected and
7
+ how to disable it before a later run.
11
8
 
12
9
  Notice is written to stderr so it doesn't pollute stdout output.
13
10
  """
@@ -19,18 +16,17 @@ import sys
19
16
 
20
17
  _NOTICE = """\
21
18
  \033[2m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22
- ask — anonymous telemetry is OFF
19
+ ask — anonymous telemetry is ON by default
23
20
 
24
- Nothing has been collected, and nothing will be unless you
25
- turn it on. If you would like to help:
21
+ Anonymous usage metadata is collected to improve ASK. To disable it:
26
22
 
27
- ask telemetry enable
23
+ ask telemetry disable
28
24
 
29
- If you do, this is collected: tool version, Python version,
25
+ This is collected: tool version, Python version,
30
26
  OS, commands used, flags used, MCP tool names invoked,
31
27
  approximate repo size, execution duration, errors.
32
28
 
33
- Never collected, on or off: source code, file paths, file
29
+ Never collected: source code, file paths, file
34
30
  names, secrets, tokens, environment variables, or any
35
31
  repository content.
36
32
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m
@@ -51,10 +47,10 @@ def _is_interactive() -> bool:
51
47
 
52
48
 
53
49
  def show_first_run_notice() -> None:
54
- """Print the one-time telemetry invitation on interactive terminals.
50
+ """Print the one-time telemetry disclosure on interactive terminals.
55
51
 
56
52
  Never raises. Does nothing on non-interactive / CI environments.
57
- Telemetry stays off regardless — this only tells the user the option exists.
53
+ Printing this notice does not change the telemetry setting.
58
54
  """
59
55
  if not _is_interactive():
60
56
  return
@@ -52,7 +52,8 @@ class TelemetryEvent:
52
52
  py — Python major.minor (e.g. "3.11")
53
53
  os — OS family: linux | macos | windows | other
54
54
  arch — CPU architecture: x64 | arm64 | other
55
- cmd — command: analyze | prepare-context | telemetry
55
+ cmd — command family: analyze | prepare-context | telemetry | mcp | unknown
56
+ command — closed product command identifier, or None
56
57
  flags — flag names only (no values)
57
58
  output_fmt — json | yaml
58
59
  repo_size — tiny | small | medium | large | huge
@@ -74,6 +75,7 @@ class TelemetryEvent:
74
75
  os: str = ""
75
76
  arch: str = ""
76
77
  cmd: str = ""
78
+ command: Optional[str] = None
77
79
  flags: list[str] = field(default_factory=list)
78
80
  output_fmt: str = "json"
79
81
  repo_size: str = "unknown"
@@ -57,6 +57,15 @@ _SAFE_FLAGS: frozenset[str] = frozenset({
57
57
  _SAFE_OS: frozenset[str] = frozenset({"linux", "macos", "windows", "other"})
58
58
  _SAFE_ARCH: frozenset[str] = frozenset({"x64", "arm64", "other"})
59
59
  _SAFE_CMD: frozenset[str] = frozenset({"analyze", "prepare-context", "telemetry", "mcp", "unknown"})
60
+ _SAFE_COMMANDS: frozenset[str] = frozenset({
61
+ "analyze", "prepare-context", "telemetry", "mcp", "cache", "baseline", "auth", "retrieve",
62
+ "repo-ir", "impact", "endpoints", "export", "validation", "delta", "contract-diff", "plan",
63
+ "compare", "trend", "timeline", "spring-audit", "verify-edit", "verify", "risk", "enrich",
64
+ "audit-report", "migrate-recipe", "data-exposure", "posture", "migrate-check", "impact-chain",
65
+ "pr-impact", "explain", "onboard", "review-pr", "fix-bug", "modernize", "rename-class",
66
+ "chunk-file", "activate", "selftest", "explain-endpoint", "regress", "version", "schema",
67
+ "config", "archetype", "cold-start",
68
+ })
60
69
  _SAFE_EVENTS: frozenset[str] = frozenset({
61
70
  "command_executed",
62
71
  "execution_completed",
@@ -153,6 +162,7 @@ def sanitize(event: TelemetryEvent) -> dict[str, Any]:
153
162
  "os": _safe_str(event.os, _SAFE_OS, "other"),
154
163
  "arch": _safe_str(event.arch, _SAFE_ARCH, "other"),
155
164
  "cmd": _safe_str(event.cmd, _SAFE_CMD, "unknown"),
165
+ "command": _safe_str(event.command or "", _SAFE_COMMANDS, "unknown") if event.command else None,
156
166
  "flags": _safe_flags(event.flags),
157
167
  "output_fmt": _safe_str(event.output_fmt, _SAFE_FMTS, "json"),
158
168
  "repo_size": _safe_str(event.repo_size, _SAFE_SIZES, "unknown"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.8.12
3
+ Version: 5.8.13
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/HarounDominique/sourcecode
6
6
  Project-URL: Repository, https://github.com/HarounDominique/sourcecode
@@ -49,7 +49,7 @@ Description-Content-Type: text/markdown
49
49
 
50
50
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
51
51
 
52
- ![Version](https://img.shields.io/badge/version-5.8.12-blue)
52
+ ![Version](https://img.shields.io/badge/version-5.8.13-blue)
53
53
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
54
54
 
55
55
  Copyright (c) 2026 Dominique Haroun. All rights reserved. ASK Engine 5.7.0 and
@@ -151,7 +151,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
151
151
  # pip / pipx
152
152
  pipx install sourcecode # or: pip install sourcecode
153
153
 
154
- ask version # ask 5.8.12 — and, on a build that has aged,
154
+ ask version # ask 5.8.13 — and, on a build that has aged,
155
155
  # how many releases have probably shipped since
156
156
  ```
157
157
 
@@ -580,18 +580,16 @@ ask config # version, config file path, telemetry status
580
580
  ask config /repo # + the repository's own sourcecode.config.json:
581
581
  # data labels, custom security annotations,
582
582
  # and any declaration that could not be used
583
- ask telemetry enable # anonymous telemetry is OFF by default (opt-in)
583
+ ask telemetry enable # ensure anonymous telemetry is enabled
584
584
  ```
585
585
 
586
- **Nothing is collected or transmitted unless you turn telemetry on** — not on the first run,
587
- not in CI. If you do opt in, it collects version, OS, commands, flags, duration, repo-size
588
- range, and errors: **no source code, paths, secrets, or output**. Turn it off again with
586
+ **Anonymous telemetry is ON by default**. It collects version, OS, commands, flags, duration,
587
+ repo-size range, and errors: **no source code, paths, secrets, or output**. Turn it off with
589
588
  `ask telemetry disable`, `export SOURCECODE_TELEMETRY=0`, or `DO_NOT_TRACK=1`.
590
589
 
591
- > **Auditing someone else's code — regulated, client-owned or public-sector?** Nothing to do.
592
- > Telemetry was opt-out until 3.3.0; it is opt-in now (`docs/DEFECT-LEDGER.md` P-1), because a
593
- > default you must remember to disable is the wrong default for third-party code. An explicit
594
- > choice you made before is unchanged.
590
+ > **Auditing someone else's code — regulated, client-owned or public-sector?** Set
591
+ > `SOURCECODE_TELEMETRY=0` before the run. The setting is explicit, local and honored before
592
+ > any event is sent.
595
593
 
596
594
  **A question is not a write — `--no-write` / `ASK_READONLY=1`.** One contract over
597
595
  every command: nothing is created inside the analysed repository, and a command
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=Q8fdci0rXXKy6QVw6aRGJjGkWAMdn3wJ1bKYNOpngK4,309
1
+ sourcecode/__init__.py,sha256=WDXhi5kHWMfE8cOqlfy1cyhggsaPJwg3cmS3GE08CxA,309
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/answer_bounds.py,sha256=_51_DCuYEUmI_xwUFxDMiprSavkNakkLiIBbPjevQ3M,9793
4
4
  sourcecode/archetype.py,sha256=uubguXP5ltQt25ACNVm9kX4KRTe7JCqkFfg4H9zckCo,40971
@@ -25,7 +25,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
25
25
  sourcecode/change_plan.py,sha256=wJ896V27a3qb-2vcZhrcOSOqFAwCmEyzbhdbdxM2LlQ,12504
26
26
  sourcecode/cir_graphs.py,sha256=RytqSv1WF8kdlfXj2fPQz16LY4akS2kyThbdWL8beCQ,14835
27
27
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
28
- sourcecode/cli.py,sha256=BPjurFlS8pfrQ7fiYUMZM6JGma79pdn6-q9-A-JMlHo,726997
28
+ sourcecode/cli.py,sha256=cAYOBP_CgY4Z7FjciLs2Awtkjg8cGsfjD-ULAqIRqXg,727830
29
29
  sourcecode/client_calls.py,sha256=YOPnfRZzRyqmdHvXSVeiunW8cuiuCuWXacNIQRgKMCw,22640
30
30
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
31
31
  sourcecode/compare.py,sha256=Lj8epBqrHLWBhiccE2X0ZbaP2H3hNb7qecoN9Jvsj7c,14794
@@ -117,7 +117,7 @@ sourcecode/reconciliation.py,sha256=pmqc0JNcAagKGWDcjH2rLDlKlAbfJCVUj86vV--YEXQ,
117
117
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
118
118
  sourcecode/reference_facts.py,sha256=mx7H-Ax5y8sKAQHrnJCXlgjkUzqZtQTByv9Zy2lxKQQ,15500
119
119
  sourcecode/regress.py,sha256=aXN6n7t39OOSOUvUs4HTH7UGVwl-tzhjXjHfDXNtp04,11162
120
- sourcecode/release_info.py,sha256=EtmaGDnLZlbqvjMB0i4UAjGLbKage5rPvQetuCHR20w,6064
120
+ sourcecode/release_info.py,sha256=l9HiTnxDaaMobb1XUTXACXjPxIzZyRScSkgwcnQ8OjU,6064
121
121
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
122
122
  sourcecode/remediation_diff.py,sha256=fGYqPmQF7bO54L8ivztX5PgmW4fVu54gNcG6OEQ192U,9730
123
123
  sourcecode/remedies.py,sha256=V2_cD4pLq8bKRkzF-3RGFoiu1xWYZS384WDVp1MrlZw,10225
@@ -203,8 +203,8 @@ sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,17
203
203
  sourcecode/mcp/dependency_import.py,sha256=wfvw1rmDE3n2mHDj-yHnCuy9HZ7TtPqsHG2BU-Scl1k,4146
204
204
  sourcecode/mcp/orchestrator.py,sha256=imbOxkzsQh91as8a3OdSU7VG2L3A1faUldiKwA2jMSQ,36825
205
205
  sourcecode/mcp/registry.py,sha256=kydzpUGGKckWM_F_GeGkWG1Z_qU3R8mZSbBWYKtDewU,69326
206
- sourcecode/mcp/runner.py,sha256=RGimCwhLyHxjCvH7q72y12HWEnATXZLC-NBWhkzlu0I,2852
207
- sourcecode/mcp/server.py,sha256=4YjfGUVRWrlOMe1pMkKBdUhjc9XMgDdnMcb7HrCQmCQ,67573
206
+ sourcecode/mcp/runner.py,sha256=dRvI0xS2wa8DH0diYjJiAoaGeVdtf389PekWwwh8wRI,3083
207
+ sourcecode/mcp/server.py,sha256=1CRISYBwPuAdSaNFVeupjYr2nfK6hmBv8J1nfdCLQJ8,67510
208
208
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
209
209
  sourcecode/mcp/onboarding/applier.py,sha256=Sx9vHTaXr_M1Bs4uMjO7qa_a2_p4J9oz93oJsKQ7ds4,3298
210
210
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -234,19 +234,19 @@ sourcecode/schemas/envelope-v1.schema.json,sha256=Zu5D2cspa_biP5-JMWY0H6oYGyAyA4
234
234
  sourcecode/schemas/pr-impact-v1.schema.json,sha256=fC4DLqY--N5iJpm5wCxbUeILianYbhNDNYV3n4D5gFg,3756
235
235
  sourcecode/schemas/verify-edit-v1.schema.json,sha256=0jp-yoFIdmfq-4IrzrOPCAsEN7JcKg6cjq7-87_Ez0U,3537
236
236
  sourcecode/schemas/verify-v1.schema.json,sha256=rY1h_ub3hMAKCHO6WmnyKDtI4b37xLAEC6uuoEhKR64,3441
237
- sourcecode/telemetry/__init__.py,sha256=ZxV5AJAK5_pk0RnW0B1ntpGPRapOX_XKFJdT_6RuYhE,3502
238
- sourcecode/telemetry/config.py,sha256=HD5ccO_k91WxDsnMo1lmxaF906bK3nrLv8qzUR3HbTo,4212
239
- sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8co,2472
240
- sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
241
- sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
237
+ sourcecode/telemetry/__init__.py,sha256=5jg6mIrhnt59FgxQsQcihxkHCWM07UFQFcuGwMPa9v8,3489
238
+ sourcecode/telemetry/config.py,sha256=QPuTK-oc-Bg_EQftyFxLHPSnLRiZ5NegWYfX4BcD8iI,3842
239
+ sourcecode/telemetry/consent.py,sha256=-Wb8BFGXEexOs8M0Eja7F-T6ZtLNPeRoZnHcHvlP7-Q,2153
240
+ sourcecode/telemetry/events.py,sha256=l3Z2SQUO4_C2D2ItXOOdVNimAmHedUHnUxqYFTxnsAE,2900
241
+ sourcecode/telemetry/filters.py,sha256=-lqT2oorECuqEIdeHb--7AZ6BCaUx0bya720a-Z5x6w,7335
242
242
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
243
- sourcecode/_docs/DEFECT-LEDGER.md,sha256=eD0lIU59OtcDZow3n6TDuXeO3TWEzG3YeGi7qox65i8,742831
243
+ sourcecode/_docs/DEFECT-LEDGER.md,sha256=KAUiNrCH5Az8129zeECJtgsv1C3CKngpoJuBzoM5uEA,742903
244
244
  sourcecode/_docs/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
245
245
  sourcecode/_docs/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
246
- sourcecode/_docs/USER_GUIDE.md,sha256=u6BDmr3Kv9l10u5X7xpkP8fYf-aA-BreovysiPFaYTA,87104
247
- sourcecode-5.8.12.dist-info/METADATA,sha256=QHhr0IK_Y1Nj9KJrieY-vs-xH60_Qr-dhDE-wHi4pt0,51594
248
- sourcecode-5.8.12.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
249
- sourcecode-5.8.12.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
250
- sourcecode-5.8.12.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
251
- sourcecode-5.8.12.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
252
- sourcecode-5.8.12.dist-info/RECORD,,
246
+ sourcecode/_docs/USER_GUIDE.md,sha256=yylOietj_jwPaOc_WQCkanFhoUV57HJBMDJBtA_g1jA,87303
247
+ sourcecode-5.8.13.dist-info/METADATA,sha256=TXvApuuSukNSBO_SAM-rgK7dS-yjXQcFpu4Ssj5FP50,51372
248
+ sourcecode-5.8.13.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
249
+ sourcecode-5.8.13.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
250
+ sourcecode-5.8.13.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
251
+ sourcecode-5.8.13.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
252
+ sourcecode-5.8.13.dist-info/RECORD,,