sourcecode 5.9.30__py3-none-any.whl → 5.9.32__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.9.30"
7
+ __version__ = "5.9.32"
@@ -1,5 +1,5 @@
1
1
  """Generated at build time by hatch_build.py. Do not edit (AUD-592-D04)."""
2
2
 
3
- BUILD_COMMIT = '793177c8ed2a19311a6951f0ef42ac6db158c8af'
3
+ BUILD_COMMIT = '904d896034a9ad785a80f37014d87e2b412cf1bd'
4
4
  CLOSURE_PROVENANCE = {'AUD-611-B05': {'commit': '', 'status': 'uncited'}, 'AUD-611-B06': {'commit': '', 'status': 'uncited'}, 'AUD-611-B07': {'commit': '', 'status': 'uncited'}, 'AUD-611-B08': {'commit': '', 'status': 'uncited'}, 'AUD-611-B09': {'commit': '', 'status': 'uncited'}, 'AUD-611-B10': {'commit': '', 'status': 'uncited'}, 'AUD-611-B11': {'commit': '', 'status': 'uncited'}, 'AUD-612-07': {'commit': '', 'status': 'uncited'}, 'AUD-612-09': {'commit': '', 'status': 'uncited'}, 'AUD-612-14': {'commit': '', 'status': 'uncited'}, 'AUD-612-B03': {'commit': '', 'status': 'uncited'}, 'AUD-612-B07': {'commit': '', 'status': 'uncited'}, 'AUD-612-B13': {'commit': '', 'status': 'uncited'}, 'AUD-612-E': {'commit': '', 'status': 'uncited'}, 'AUD-612-G': {'commit': '', 'status': 'uncited'}, 'AUD-612-I': {'commit': '', 'status': 'uncited'}, 'AUD-612-K': {'commit': '', 'status': 'uncited'}, 'AUD-612-M': {'commit': '', 'status': 'uncited'}, 'AUD-612-R': {'commit': '', 'status': 'uncited'}, 'AUD-613-ROOT': {'commit': '', 'status': 'uncited'}, 'AUD-614-07': {'commit': '', 'status': 'uncited'}, 'AUD-614-E': {'commit': '', 'status': 'uncited'}, 'AUD-614-F': {'commit': '', 'status': 'uncited'}, 'AUD-614-G': {'commit': '', 'status': 'uncited'}, 'AUD-614-I': {'commit': '', 'status': 'uncited'}, 'AUD-614-INTENT-TIE': {'commit': '', 'status': 'uncited'}, 'AUD-614-JUNIT4': {'commit': '', 'status': 'uncited'}, 'AUD-614-RISK-L0': {'commit': '', 'status': 'uncited'}, 'AUD-614-SELFTEST-TIMEOUT': {'commit': '', 'status': 'uncited'}, 'AUD-615-07': {'commit': '', 'status': 'uncited'}, 'AUD-615-COLD-COMPACT': {'commit': '', 'status': 'uncited'}, 'AUD-615-DRIFT': {'commit': '', 'status': 'uncited'}, 'AUD-615-E': {'commit': '', 'status': 'uncited'}, 'AUD-615-F': {'commit': '', 'status': 'uncited'}, 'AUD-615-INTENT-TIE': {'commit': '', 'status': 'uncited'}, 'AUD-615-JUNIT4': {'commit': '', 'status': 'uncited'}, 'AUD-615-RISK-L0': {'commit': '', 'status': 'uncited'}, 'AUD-615-SELFTEST-TIMEOUT': {'commit': '', 'status': 'uncited'}, 'AUD-616-E-PROV': {'commit': '', 'status': 'uncited'}, 'AUD-616-F, -RISK-L0, -SELFTEST-TIMEOUT, -INTENT-TIE, -DRIFT, -COLD-COMPACT, -E, -07': {'commit': '', 'status': 'uncited'}, 'AUD-616-INTENT-TIE-COV': {'commit': '', 'status': 'uncited'}, 'AUD-616-JUNIT4-SELF': {'commit': '', 'status': 'uncited'}, 'AUD-616-KOTLIN': {'commit': '', 'status': 'uncited'}, 'AUD-616-O': {'commit': '', 'status': 'uncited'}, 'AUD-616-P': {'commit': '', 'status': 'uncited'}, 'Suite hygiene': {'commit': '', 'status': 'uncited'}, 'Window': {'commit': '', 'status': 'uncited'}, '`ALT-08` (arithmetic half)': {'commit': '', 'status': 'uncited'}, '`ALT-25`': {'commit': '', 'status': 'uncited'}, '`ASK-FIELD-01`': {'commit': '829f9fa', 'status': 'present'}, '`ASK-FIELD-02`': {'commit': '6a659fa', 'status': 'present'}, '`ASK-FIELD-03`': {'commit': '55b0d51', 'status': 'present'}, '`AUD-588-F05`': {'commit': '470e4a4', 'status': 'present'}, '`AUD-590-B01` / `B-01`, `F-01`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B02` / `B-02`, `F-02`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B03` / `B-03`, `F-03`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B04` / `B-04`, `F-04`': {'commit': '', 'status': 'uncited'}, '`AUD-590-R01` / `R-01`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A01` / `A-1`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A02` / `A-2`, `A-4`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A03` / `A-3`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A05` / `A-5`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A06` / `A-6`, `A-7`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A08` / `A-8`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A09` / `A-9`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A10` / `A-10`': {'commit': '', 'status': 'uncited'}, '`AUD-592-A02` / `A-1` second half, `§12.5`': {'commit': '63f3d61', 'status': 'present'}, '`AUD-592-B01` / `B-8`': {'commit': '8329614', 'status': 'present'}, '`AUD-592-D01` / `D-1`': {'commit': '0f8e03f', 'status': 'present'}, '`AUD-592-D02` / `D-2`': {'commit': '1d3db4d', 'status': 'present'}, '`AUD-592-D03` / `D-3`': {'commit': 'f28fe40', 'status': 'present'}, '`AUD-592-R01` / `A-1`': {'commit': '216025f', 'status': 'present'}, '`AUD-592-R02` / `N-4`, reopens `AUD-513-N04`': {'commit': 'ec3050d', 'status': 'present'}, '`AUD-593-N01` / `N-01`': {'commit': 'dd6a706', 'status': 'present'}, '`AUD-593-N02` / `N-02`': {'commit': 'f539787', 'status': 'present'}, '`AUD-593-N03` / `N-03`': {'commit': '1dd0b1c', 'status': 'present'}, '`AUD-593-N05` / `N-05`': {'commit': 'aa89388', 'status': 'present'}, '`AUD-593-N06` / `N-06`': {'commit': 'aa584a6', 'status': 'present'}, '`AUD-594-N04` / `D-5`, disclosure half of `B-3`': {'commit': 'a0060b7', 'status': 'present'}, '`AUD-594-X01` / `A-1`, residual of `AUD-593-N03`': {'commit': 'dd222b9', 'status': 'present'}, '`AUD-594-X02` / `A-1` second half': {'commit': '1193937', 'status': 'present'}, '`AUD-594-X03` / `X-03`, residual of `AUD-593-N01`': {'commit': 'e89e8f0', 'status': 'present'}, '`AUD-594-X04`': {'commit': '05b8cc9', 'status': 'present'}, '`AUD-595-A02` / `N-5`, advisory half of `AUD-588-B11`': {'commit': '3e15227', 'status': 'present'}, '`AUD-595-A03` / `B-6` exit-code half': {'commit': 'ab7285f', 'status': 'present'}, '`AUD-595-A05`': {'commit': 'e0b3d10', 'status': 'present'}, '`AUD-595-A07`, narrow half of `AUD-592-A02`': {'commit': 'cc15177', 'status': 'present'}, '`AUD-595-B01` / §B': {'commit': 'b77a1ad', 'status': 'present'}, '`AUD-595-Q01`': {'commit': '91a6566', 'status': 'present'}, '`AUD-596-A01` / `ASK-DET-001`': {'commit': 'fa508b4', 'status': 'present'}, '`AUD-596-A02` / `ASK-SELF-001`': {'commit': '4dfadc8', 'status': 'present'}, '`AUD-596-A03` / `ASK-C1-001`': {'commit': 'bffa3a6', 'status': 'present'}, '`AUD-596-A04` / `ASK-UX-003` + `ASK-UX-004`': {'commit': 'f0aac3c', 'status': 'present'}, '`AUD-596-B01` / `B-01`, harder witness for `BUG-6`': {'commit': '567d09d', 'status': 'present'}, '`AUD-596-B02` / `B-02`, class residual of `R2`': {'commit': '', 'status': 'uncited'}, '`AUD-596-B03` / `B-03`': {'commit': 'a8cb814', 'status': 'present'}, '`AUD-596-B06` / `B-06`': {'commit': '8107927', 'status': 'present'}, '`AUD-596-B07` / `B-07`, class residual of `AUD-592-R02`': {'commit': 'e060485', 'status': 'present'}, '`AUD-596-B09` / `B-09`': {'commit': '5b8146c', 'status': 'present'}, '`AUD-596-B10` / `B-10`': {'commit': '5cb994d', 'status': 'present'}, '`AUD-596-B16` / `B-16`': {'commit': '1507f69', 'status': 'present'}, '`AUD-596-D01` / `B-13` + `B-14`': {'commit': '0b1bdf0', 'status': 'present'}, '`AUD-596-D02` / `B-08`, residual of `AUD-594-N04`': {'commit': 'e7f73c3', 'status': 'present'}, '`AUD-596-D03` / `ASK-AGT-001`': {'commit': '8c46967', 'status': 'present'}, '`AUD-596-D04` / `B-18`': {'commit': '040f027', 'status': 'present'}, '`AUD-596-D05` / `B-19`': {'commit': 'fd2bf68', 'status': 'present'}, '`AUD-596-D06` / `B-20`': {'commit': '784cd83', 'status': 'present'}, '`AUD-596-D07` / `B-17`, half refuted': {'commit': 'c3ff822', 'status': 'present'}, '`AUD-596-D08` / `B-22`, class residual of `AUD-591-A06`': {'commit': 'b396c6a', 'status': 'present'}, '`AUD-596-D09` / `B-23`, surviving half of `B26`': {'commit': 'fd3d63f', 'status': 'present'}, '`AUD-596-D10` / `B-29`': {'commit': '', 'status': 'uncited'}, '`AUD-596-D11` / `ASK-UX-005`': {'commit': 'f4b4f77', 'status': 'present'}, '`AUD-596-D12` — six hints that send the reader back to the error': {'commit': '498df44', 'status': 'present'}, '`AUD-596-D13` / `B-28`': {'commit': '1014c72', 'status': 'present'}, '`AUD-596-D14` / `B-15`, narrow half of `B20`': {'commit': '10f2827', 'status': 'present'}, '`AUD-596-R01` / `B-05`': {'commit': '', 'status': 'uncited'}, '`AUD-596-X01` / `ASK-GATE-001`, gating half of `B24`': {'commit': 'e59c655', 'status': 'present'}, '`AUD-596-X02` / `ASK-UX-001` + `ASK-UX-002` + `B-21`': {'commit': 'f6ebf50', 'status': 'present'}, '`AUD-597-A01` / `ASK-UX-001` + `N-01`': {'commit': 'f8cf061', 'status': 'present'}, '`AUD-597-A02` / `ASK-UX-008` + `ASK-DOC-002`': {'commit': '9d47a4c', 'status': 'present'}, '`AUD-597-A03` / `ASK-DET-001`': {'commit': '4c7622a', 'status': 'present'}, '`AUD-597-A04` / `ASK-LEDGER-001`': {'commit': '46fb870', 'status': 'present'}, '`AUD-597-D01` / `B-04`': {'commit': '1094e6a', 'status': 'present'}, '`AUD-597-D02` / `ASK-DOC-001`': {'commit': 'ecb1bb7', 'status': 'present'}, '`AUD-597-D03` / `N-06`': {'commit': '9784333', 'status': 'present'}, '`AUD-597-D04` / `N-05`': {'commit': 'e51c087', 'status': 'present'}, '`AUD-597-D05` / `N-04`, fourth appearance of `C4-27`': {'commit': '9cccc15', 'status': 'present'}, '`AUD-597-D06` / `ASK-CLI-001`': {'commit': '178aa6d', 'status': 'present'}, '`AUD-597-D07` / `N-03`': {'commit': '9245e35', 'status': 'present'}, '`AUD-597-R01` / `ASK-PERF-001`': {'commit': '6910054', 'status': 'present'}, '`AUD-597-R02` / `ASK-ENC-001`': {'commit': 'ce6dac1', 'status': 'present'}, '`AUD-597-R03` / `ASK-UX-002`': {'commit': 'c57e50b', 'status': 'present'}, '`AUD-597-X01` / `ASK-BUILD-001` + `ASK-META-001` / `B-12`': {'commit': '354338b', 'status': 'present'}, '`AUD-598-B01` / `ASK-META-001`': {'commit': 'b2f7c65', 'status': 'present'}, '`AUD-598-B02` / `ASK-UX-002`': {'commit': '56a4bf7', 'status': 'present'}, '`AUD-598-B03` / `B-11`': {'commit': 'c63cb24', 'status': 'present'}, '`AUD-598-B04` / `N-02`': {'commit': '187937f', 'status': 'present'}, '`AUD-598-R01` / `ASK-PERF-002`': {'commit': '', 'status': 'uncited'}, '`AUD-598-X01` / `N-01` + `B-13` + `B-14` + `B-29` + `N-06`': {'commit': '6f2f5a4', 'status': 'present'}, '`AUD-600-B01` / `P-01`': {'commit': '366ffed', 'status': 'present'}, '`AUD-600-B02` / `P-02` + `ASK-PACK-002`': {'commit': '2a814c3', 'status': 'present'}, '`AUD-600-B03` / `ASK-PACK-001`': {'commit': '452f2a0', 'status': 'present'}, '`AUD-600-D01`': {'commit': '1224e1b', 'status': 'present'}, '`AUD-600-D02`': {'commit': 'eab8172', 'status': 'present'}, '`AUD-600-R01` / `R-01`': {'commit': '88add64', 'status': 'present'}, '`AUD-601-B01` / `N-02`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-B02` / `ASK-PACK-004`': {'commit': '', 'status': 'uncited'}, '`AUD-601-D01` / `N-01`, `ASK-UX-001`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-D02` / `B-13`, `B-14`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-M01` / `B-26`, `B-27` retracted': {'commit': '', 'status': 'uncited'}, '`AUD-601-R01` / `R-02`': {'commit': '', 'status': 'uncited'}, '`AUD-619-WATCH`': {'commit': '', 'status': 'uncited'}, '`AUD-620-COMMAND-SURFACE`': {'commit': '', 'status': 'uncited'}, '`AUD-620-REVIEW-PR`': {'commit': '', 'status': 'uncited'}, '`AUD-621-REVIEW-PR`': {'commit': '', 'status': 'uncited'}, '`B-3`': {'commit': '773268d', 'status': 'present'}, '`B-6`': {'commit': '17c6e06', 'status': 'present'}, '`B-8`': {'commit': '8329614', 'status': 'present'}, '`BUG-03` (statement half)': {'commit': '', 'status': 'uncited'}, '`BUG-09`': {'commit': '', 'status': 'uncited'}, '`BUG-10` (Vert.x residual)': {'commit': '', 'status': 'uncited'}, '`BUG-13`': {'commit': '', 'status': 'uncited'}, '`BUG-13`, `BUG-15`, `BUG-17`, `BUG-21`, `BUG-08`, `BUG-10`/`BUG-23`': {'commit': '', 'status': 'uncited'}, '`BUG-14`': {'commit': '', 'status': 'uncited'}, '`BUG-15`': {'commit': '', 'status': 'uncited'}, '`BUG-16`': {'commit': '', 'status': 'uncited'}, '`BUG-20`': {'commit': '', 'status': 'uncited'}, '`BUG-21`': {'commit': '', 'status': 'uncited'}, '`BUG-3` / `443b345`': {'commit': '', 'status': 'uncited'}, '`BUG-4a` / `AUD-588-B11` residual': {'commit': '24b4185', 'status': 'present'}, '`BUG-4b` / `AUD-588-B11` residual': {'commit': '0376350', 'status': 'present'}, '`E-39`, class residual of `AUD-593-N06`': {'commit': '25fc11f', 'status': 'present'}, '`N-5`': {'commit': 'c68083f', 'status': 'present'}, '`N-7`': {'commit': '082e9de', 'status': 'present'}, '`NEW-01` (regime row)': {'commit': '', 'status': 'uncited'}, '`NEW-02` / `BUG-05`': {'commit': '', 'status': 'uncited'}, '`R-01-INFLIGHT` (stage capture)': {'commit': '', 'status': 'uncited'}, '`R-1`': {'commit': '424fb1c', 'status': 'present'}, '`R-2`': {'commit': '5158b92', 'status': 'present'}, '`R-3`': {'commit': '88aec04', 'status': 'present'}, '`R-4`': {'commit': '04fd04f', 'status': 'present'}, '`REG-01`': {'commit': '', 'status': 'uncited'}, '`REG-04`': {'commit': '', 'status': 'uncited'}, '`SEC-010` cost': {'commit': '', 'status': 'uncited'}, '`retrieve security-surface --target`': {'commit': '', 'status': 'uncited'}}
5
5
  CLOSURE_COVERAGE = {'closed_rows': 185, 'present': 91, 'absent': 0, 'unresolved': 0, 'uncited': 94, 'unknown': 0}
@@ -279,7 +279,7 @@ pipx install sourcecode # isolated install, no venv needed
279
279
 
280
280
  # Verify
281
281
  ask version
282
- # ask 5.9.30
282
+ # ask 5.9.32
283
283
  ```
284
284
 
285
285
  Requires Python 3.9+.
sourcecode/cli.py CHANGED
@@ -1790,6 +1790,8 @@ def _emit_error_json(error: str, message: str, **context: object) -> None:
1790
1790
  All CLI validation and runtime errors must go through this helper so that
1791
1791
  agents and tools can parse stderr reliably regardless of error type.
1792
1792
  """
1793
+ from sourcecode.telemetry.lifecycle import mark
1794
+ mark("internal_error" if error in {"INTERNAL_ERROR", "EXECUTION_FAILED"} else "expected_error")
1793
1795
  import json as _json
1794
1796
  payload = build_error_envelope(error, message, **context)
1795
1797
  sys.stderr.write(_json.dumps(payload, ensure_ascii=_json_ensure_ascii()) + "\n")
@@ -4863,17 +4865,14 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
4863
4865
  "enrich", "audit-report", "audit-verify", "migrate-recipe", "data-exposure", "posture", "migrate-check",
4864
4866
  "impact-chain", "pr-impact", "explain", "onboard", "review-pr", "fix-bug", "modernize",
4865
4867
  "rename-class", "chunk-file", "activate", "selftest", "explain-endpoint", "regress", "version",
4866
- "schema", "config", "archetype", "cold-start", "telemetry", "mcp", "cache", "baseline",
4868
+ "schema", "config", "update", "archetype", "cold-start", "telemetry", "mcp", "cache", "baseline",
4867
4869
  "auth", "retrieve", "prepare-context", "pack",
4868
4870
  })
4869
4871
 
4870
4872
  def _telemetry_command(args_for_command: Optional[list[str]]) -> str:
4871
4873
  """Extract only a registered CLI command, never a path or option value."""
4872
4874
  tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
4873
- for token in tokens:
4874
- if token in _TELEMETRY_COMMANDS:
4875
- return token
4876
- return "analyze"
4875
+ return tokens[0] if tokens and tokens[0] in _TELEMETRY_COMMANDS else "analyze"
4877
4876
 
4878
4877
  def _telemetry_flags(args_for_command: Optional[list[str]]) -> list[str]:
4879
4878
  tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
@@ -4896,25 +4895,59 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
4896
4895
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
4897
4896
  started = time.monotonic()
4898
4897
  command = _telemetry_command(args)
4899
- success = True
4898
+ from sourcecode import telemetry as _tel
4899
+ from sourcecode.telemetry.lifecycle import Execution, current, exit_outcome
4900
+ parent = current.get()
4901
+ scope = Execution(interface=parent.interface if parent else "cli",
4902
+ parent=parent.id if parent else "", command=command)
4903
+ token = current.set(scope)
4904
+ fields = dict(cmd=("mcp" if command == "mcp" else "telemetry" if command == "telemetry" else "analyze"),
4905
+ command=command, flags=_telemetry_flags(args))
4906
+ _tel.record("execution_started", **fields, outcome="started")
4907
+ error_kind = None
4900
4908
  try:
4901
- return _orig_cmd_main(args=args, **kwargs)
4902
- except BaseException:
4903
- success = False
4909
+ result = _orig_cmd_main(args=args, **kwargs)
4910
+ # Click standalone_mode=False returns the exit code for explicit Exit.
4911
+ if isinstance(result, int) and not isinstance(result, bool):
4912
+ scope.outcome = exit_outcome(result, scope)
4913
+ return result
4914
+ except SystemExit as exc:
4915
+ scope.outcome = exit_outcome(exc.code, scope)
4916
+ import click
4917
+ cause = exc.__context__
4918
+ seen = set()
4919
+ while cause is not None and id(cause) not in seen:
4920
+ seen.add(id(cause))
4921
+ if isinstance(cause, KeyboardInterrupt):
4922
+ scope.outcome = "cancelled"
4923
+ error_kind = "KeyboardInterrupt"
4924
+ break
4925
+ if isinstance(cause, click.ClickException) and scope.outcome == "unknown_error":
4926
+ scope.outcome = "expected_error"
4927
+ cause = cause.__context__
4928
+ if scope.outcome in {"expected_error", "unknown_error"}:
4929
+ error_kind = "SystemExit"
4930
+ raise
4931
+ except BaseException as exc:
4932
+ import click
4933
+ scope.outcome = ("cancelled" if isinstance(exc, (KeyboardInterrupt, click.Abort))
4934
+ else "expected_error" if isinstance(exc, click.ClickException)
4935
+ else "internal_error")
4936
+ error_kind = type(exc).__name__
4904
4937
  raise
4905
4938
  finally:
4906
4939
  try:
4907
- from sourcecode import telemetry as _tel
4940
+ ok = scope.outcome in {"succeeded", "negative"}
4908
4941
  _tel.record(
4909
- "execution_completed",
4910
- cmd=("mcp" if command == "mcp" else "telemetry" if command == "telemetry" else "analyze"),
4911
- command=command,
4912
- flags=_telemetry_flags(args),
4913
- duration_s=time.monotonic() - started,
4914
- success=success,
4942
+ "execution_completed" if ok else "execution_failed",
4943
+ **fields, duration_s=time.monotonic() - started,
4944
+ success=ok, outcome=scope.outcome, error_kind=error_kind,
4915
4945
  )
4916
4946
  except Exception:
4917
4947
  pass
4948
+ if parent and scope.outcome != "succeeded":
4949
+ parent.outcome = scope.outcome
4950
+ current.reset(token)
4918
4951
  if _previous_invocation is None:
4919
4952
  _tls.__dict__.pop("invocation_args", None)
4920
4953
  else:
@@ -13621,6 +13654,9 @@ def verify_edit_cmd(
13621
13654
  output = _serialize_dict(v.to_dict(), format)
13622
13655
  _emit_command_output(output, output_path, copy,
13623
13656
  success_msg=f"verify-edit verdict written to {output_path}")
13657
+ if v.exit_code != 0:
13658
+ from sourcecode.telemetry.lifecycle import mark
13659
+ mark("negative" if v.exit_code == 1 else "expected_error")
13624
13660
  if ci and v.exit_code != 0:
13625
13661
  raise typer.Exit(code=v.exit_code)
13626
13662
 
@@ -13964,6 +14000,9 @@ def verify_cmd(
13964
14000
  copy,
13965
14001
  success_msg=f"verify report written to {output_path}",
13966
14002
  )
14003
+ if report.exit_code != 0:
14004
+ from sourcecode.telemetry.lifecycle import mark
14005
+ mark("negative" if report.exit_code == 1 else "expected_error")
13967
14006
  if ci and report.exit_code != 0:
13968
14007
  raise typer.Exit(code=report.exit_code)
13969
14008
 
@@ -14785,6 +14824,8 @@ def audit_verify_cmd(
14785
14824
  success_msg="audit signature verified" if verdict["valid"] else "audit signature mismatch",
14786
14825
  )
14787
14826
  if not verdict["valid"]:
14827
+ from sourcecode.telemetry.lifecycle import mark
14828
+ mark("negative")
14788
14829
  raise typer.Exit(code=1)
14789
14830
 
14790
14831
 
@@ -15176,6 +15217,8 @@ def migrate_apply_cmd(
15176
15217
  ),
15177
15218
  )
15178
15219
  if ci and data["gate"]["exit_code"]:
15220
+ from sourcecode.telemetry.lifecycle import mark
15221
+ mark("negative")
15179
15222
  raise typer.Exit(code=1)
15180
15223
  return
15181
15224
 
@@ -16223,6 +16266,9 @@ def migrate_check_cmd(
16223
16266
  )
16224
16267
  if ci:
16225
16268
  _gate = (payload or report.to_dict()).get("metadata", {}).get("gate", {})
16269
+ if _gate.get("exit_code", 0):
16270
+ from sourcecode.telemetry.lifecycle import mark
16271
+ mark("negative")
16226
16272
  raise typer.Exit(code=int(_gate.get("exit_code", 0)))
16227
16273
 
16228
16274
 
@@ -16739,6 +16785,8 @@ def pr_impact_cmd(
16739
16785
  # UNKNOWN means the analysis could not see everything it was asked about.
16740
16786
  # A gate must not read that as "below threshold".
16741
16787
  if report.risk_level == "UNKNOWN" or _actual >= _threshold:
16788
+ from sourcecode.telemetry.lifecycle import mark
16789
+ mark("negative")
16742
16790
  raise typer.Exit(code=1)
16743
16791
 
16744
16792
 
@@ -17852,8 +17900,12 @@ def self_audit_cmd(
17852
17900
  )
17853
17901
 
17854
17902
  if fail_on == "fail" and data["verdict"] == "fail":
17903
+ from sourcecode.telemetry.lifecycle import mark
17904
+ mark("negative")
17855
17905
  raise typer.Exit(code=1)
17856
17906
  if fail_on == "unknown" and data["verdict"] in ("fail", "unknown"):
17907
+ from sourcecode.telemetry.lifecycle import mark
17908
+ mark("negative")
17857
17909
  raise typer.Exit(code=1)
17858
17910
 
17859
17911
 
@@ -17993,8 +18045,12 @@ def drift_cmd(
17993
18045
  + data["counts"]["added_without_a_matching_rule"]
17994
18046
  )
17995
18047
  if fail_on_exposure and _exposed:
18048
+ from sourcecode.telemetry.lifecycle import mark
18049
+ mark("negative")
17996
18050
  raise typer.Exit(code=1)
17997
18051
  if fail_on_drift and data["drifted"]:
18052
+ from sourcecode.telemetry.lifecycle import mark
18053
+ mark("negative")
17998
18054
  raise typer.Exit(code=1)
17999
18055
 
18000
18056
 
@@ -18264,10 +18320,16 @@ def pr_risk_cmd(
18264
18320
  # that nobody could answer the question, and a gate that passes on that
18265
18321
  # is a gate that passes on a blind spot.
18266
18322
  if result.verdict == VERDICT_UNKNOWN:
18323
+ from sourcecode.telemetry.lifecycle import mark
18324
+ mark("negative")
18267
18325
  raise typer.Exit(code=1)
18268
18326
  if fail_on == "block" and result.verdict == VERDICT_BLOCK:
18327
+ from sourcecode.telemetry.lifecycle import mark
18328
+ mark("negative")
18269
18329
  raise typer.Exit(code=1)
18270
18330
  if fail_on == "review" and result.verdict in (VERDICT_BLOCK, VERDICT_REVIEW):
18331
+ from sourcecode.telemetry.lifecycle import mark
18332
+ mark("negative")
18271
18333
  raise typer.Exit(code=1)
18272
18334
 
18273
18335
 
@@ -18681,8 +18743,12 @@ def cve_cmd(
18681
18743
  )
18682
18744
 
18683
18745
  if fail_on == "reachable" and _reachable > 0:
18746
+ from sourcecode.telemetry.lifecycle import mark
18747
+ mark("negative")
18684
18748
  raise typer.Exit(code=1)
18685
18749
  if fail_on == "any" and _affected_total > 0:
18750
+ from sourcecode.telemetry.lifecycle import mark
18751
+ mark("negative")
18686
18752
  raise typer.Exit(code=1)
18687
18753
 
18688
18754
 
@@ -19255,6 +19321,8 @@ def version_cmd(
19255
19321
  """
19256
19322
  from sourcecode.release_info import advisory_line, build_status
19257
19323
  from sourcecode.product_info import COPYRIGHT, PRODUCT, TAGLINE, build_provenance
19324
+ from sourcecode.update import check as check_for_update
19325
+ _update_status = check_for_update()
19258
19326
 
19259
19327
  if getattr(sys.stdout, "isatty", lambda: False)():
19260
19328
  _lines = [f"{PRODUCT} {__version__}", TAGLINE, COPYRIGHT]
@@ -19264,11 +19332,13 @@ def version_cmd(
19264
19332
  _advisory = advisory_line()
19265
19333
  if _advisory:
19266
19334
  _lines.append(_advisory)
19335
+ if _update_status.get("status") == "available":
19336
+ _lines.append(f"A newer ASK version is available: {_update_status['latest_version']} (run `ask update`)")
19267
19337
  _emit_text_output("\n".join(_lines), copy, output_path)
19268
19338
  else:
19269
19339
  import json as _json_ver
19270
19340
  from sourcecode.envelope import ENVELOPE_VERSION, available_schemas
19271
- _emit_text_output(_json_ver.dumps({
19341
+ _version_payload = {
19272
19342
  "cli_version": __version__,
19273
19343
  "mcp_schema_version": __version__,
19274
19344
  "compatibility_schema_version": "1.0",
@@ -19285,7 +19355,10 @@ def version_cmd(
19285
19355
  # about what this release publishes.
19286
19356
  "registries": published_registries(),
19287
19357
  "provenance": build_provenance(),
19288
- }, ensure_ascii=_json_ensure_ascii()), copy, output_path)
19358
+ }
19359
+ if _update_status.get("status") == "available":
19360
+ _version_payload["update_available"] = _update_status["latest_version"]
19361
+ _emit_text_output(_json_ver.dumps(_version_payload, ensure_ascii=_json_ensure_ascii()), copy, output_path)
19289
19362
 
19290
19363
 
19291
19364
  def published_registries() -> "list[str]":
@@ -19475,6 +19548,41 @@ def schema_cmd(
19475
19548
  )
19476
19549
 
19477
19550
 
19551
+ # ── update / config ───────────────────────────────────────────────────────────
19552
+
19553
+ @app.command("update")
19554
+ def update_cmd(
19555
+ check_only: bool = typer.Option(False, "--check", help="Check PyPI without installing."),
19556
+ yes: bool = typer.Option(False, "--yes", help="Confirm installation without prompting."),
19557
+ force: bool = typer.Option(False, "--force", help="Ignore the 24-hour check cache."),
19558
+ ) -> None:
19559
+ """Check for a newer ASK release and optionally install it."""
19560
+ from sourcecode.update import check, install
19561
+ result = check(force=force)
19562
+ if result.get("status") == "available" and not check_only:
19563
+ target = result["latest_version"]
19564
+ if not yes:
19565
+ if not sys.stdin.isatty() or not sys.stderr.isatty():
19566
+ _emit_error_json(INVALID_INPUT_CODE, "Updating requires confirmation.", hint="Run `ask update --yes` explicitly in automation.")
19567
+ raise typer.Exit(code=2)
19568
+ if not typer.confirm(f"ASK {target} is available. Install it now?", err=True):
19569
+ result = {**result, "status": "declined"}
19570
+ else:
19571
+ result = install(target)
19572
+ else:
19573
+ result = install(target)
19574
+ if result.get("status") == "available":
19575
+ message = f"ASK {result['latest_version']} is available (current: {result['current_version']}). Run `ask update` to install."
19576
+ elif result.get("status") == "current":
19577
+ message = f"ASK is up to date ({result['current_version']})."
19578
+ elif result.get("status") == "declined":
19579
+ message = "Update declined; ASK was not changed."
19580
+ elif result.get("status") == "updated":
19581
+ message = f"ASK updated to {result.get('target_version')}. Restart persistent MCP processes."
19582
+ else:
19583
+ message = f"ASK update status: {result.get('status')} ({result.get('reason', 'no additional details')})."
19584
+ _emit_command_output(json.dumps({**result, "message": message}, ensure_ascii=False), None, False, fmt="json")
19585
+
19478
19586
  # ── config ────────────────────────────────────────────────────────────────────
19479
19587
 
19480
19588
  @app.command("config")
sourcecode/license.py CHANGED
@@ -718,6 +718,7 @@ def activate_license(license_key: str) -> None:
718
718
  Outputs JSON to stdout on success; exits 0 on success, 1 on any failure.
719
719
  Errors use the CLI JSON envelope on stderr so stdout remains machine-safe.
720
720
  """
721
+ _emit_telemetry("activation_started", feature="key", outcome="started")
721
722
  if not _LICENSE_KEY_RE.match(license_key):
722
723
  _fail("invalid_license", "License key format is invalid.")
723
724
 
sourcecode/mcp/runner.py CHANGED
@@ -48,8 +48,16 @@ def run_command(args: list[str]) -> Any:
48
48
  # Pass raw args to invoke — the _cmd_main hook inside cli.py handles path
49
49
  # extraction via _preprocess_args. Pre-processing here would strip the path
50
50
  # from args, then _cmd_main would re-process the stripped list and lose it.
51
- with _runner_lock:
52
- result = _runner.invoke(app, list(args))
51
+ from sourcecode.telemetry.lifecycle import Execution, current
52
+ token = None
53
+ if current.get() is None:
54
+ token = current.set(Execution(interface="mcp", id=""))
55
+ try:
56
+ with _runner_lock:
57
+ result = _runner.invoke(app, list(args))
58
+ finally:
59
+ if token is not None:
60
+ current.reset(token)
53
61
 
54
62
  if result.exit_code != 0:
55
63
  stdout_raw = getattr(result, "output", "")
sourcecode/mcp/server.py CHANGED
@@ -63,13 +63,17 @@ def _record_tool_invocation(name: Any, success: bool, started: float) -> None:
63
63
  import time as _time
64
64
 
65
65
  from sourcecode import telemetry as _tel
66
+ from sourcecode.telemetry.lifecycle import current
67
+ scope = current.get()
66
68
 
67
69
  _tel.record(
68
70
  "mcp_tool_invoked",
69
71
  cmd="mcp",
70
72
  tool=str(name) if name else None,
71
73
  duration_s=_time.monotonic() - started,
72
- success=success,
74
+ success=success or bool(scope and scope.outcome == "negative"),
75
+ outcome=(scope.outcome if scope and scope.outcome != "succeeded"
76
+ else "succeeded" if success else "expected_error"),
73
77
  )
74
78
  except Exception:
75
79
  pass # telemetry must never affect a tool call
@@ -113,6 +117,8 @@ try:
113
117
  }
114
118
  _record_tool_invocation(self.name, False, _started)
115
119
  return _payload
120
+ from sourcecode.telemetry.lifecycle import mark
121
+ mark("internal_error")
116
122
  _record_tool_invocation(self.name, False, _started)
117
123
  raise
118
124
  _record_tool_invocation(
@@ -120,7 +126,28 @@ try:
120
126
  )
121
127
  return _result
122
128
 
123
- _FastMCPTool.run = _patched_tool_run # type: ignore[method-assign]
129
+ _dispatch_with_telemetry = _patched_tool_run
130
+
131
+ async def _scoped_tool_run(self, arguments, context=None, convert_result=False):
132
+ from sourcecode import telemetry as tel
133
+ from sourcecode.telemetry.lifecycle import Execution, current
134
+ scope = Execution(interface="mcp")
135
+ token = current.set(scope)
136
+ started = _time_mod.monotonic()
137
+ tel.record("execution_started", cmd="mcp", tool=self.name, outcome="started")
138
+ try:
139
+ return await _dispatch_with_telemetry(self, arguments, context, convert_result)
140
+ except BaseException as exc:
141
+ import asyncio
142
+ if isinstance(exc, (KeyboardInterrupt, asyncio.CancelledError)):
143
+ tel.record("mcp_tool_invoked", cmd="mcp", tool=self.name,
144
+ success=False, outcome="cancelled",
145
+ duration_s=_time_mod.monotonic() - started)
146
+ raise
147
+ finally:
148
+ current.reset(token)
149
+
150
+ _FastMCPTool.run = _scoped_tool_run # type: ignore[method-assign]
124
151
  except Exception:
125
152
  pass # never break server startup over a patch failure
126
153
 
@@ -1678,6 +1705,24 @@ def version() -> dict:
1678
1705
  return _execute(["version"])
1679
1706
 
1680
1707
 
1708
+ @mcp.tool()
1709
+ def check_for_updates(force: bool = False) -> dict:
1710
+ """Check PyPI for a newer ASK release without installing anything."""
1711
+ from sourcecode.update import check
1712
+ return {"success": True, "data": check(force=force), "error": None}
1713
+
1714
+
1715
+ @mcp.tool()
1716
+ def update_ask(confirm: bool = False, force: bool = False) -> dict:
1717
+ """Check ASK's PyPI release; install only when confirm=true."""
1718
+ from sourcecode.update import check, install
1719
+ status = check(force=force)
1720
+ if not confirm or status.get("status") != "available":
1721
+ return {"success": True, "data": {**status, "installed": False}, "error": None}
1722
+ result = install(status["latest_version"])
1723
+ return {"success": result.get("status") in {"updated", "current"}, "data": {**result, "installed": result.get("status") == "updated"}, "error": None if result.get("status") in {"updated", "current"} else result}
1724
+
1725
+
1681
1726
  @mcp.tool()
1682
1727
  def config() -> dict:
1683
1728
  """Show sourcecode CLI configuration.
@@ -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-09-02"
30
+ RELEASE_DATE = "2026-09-12"
31
31
 
32
32
  #: Releases per day, measured over the latest 30 CHANGELOG releases (the same
33
33
  #: computation the test re-runs). The nine-day interval before 5.9.0 lowers
@@ -14,12 +14,13 @@ See docs/privacy.md for full details.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import os
17
18
  import platform
18
19
  import sys
19
20
  import uuid
20
21
  from typing import Any, Optional
21
22
 
22
- from sourcecode.telemetry.config import get_install_id, is_enabled
23
+ from sourcecode.telemetry.config import get_install_id, is_enabled, telemetry_context
23
24
  from sourcecode.telemetry.events import (
24
25
  TelemetryEvent,
25
26
  duration_bucket,
@@ -30,10 +31,14 @@ from sourcecode.telemetry.transport import send
30
31
 
31
32
  # Ephemeral session identifier — new random value each process start.
32
33
  # 8 hex chars, never persisted, used only to correlate events within one run.
33
- _SESSION: str = uuid.uuid4().hex[:8]
34
+ _SESSION: str = uuid.uuid4().hex
35
+ _SESSION_PID = os.getpid()
34
36
 
35
37
 
36
38
  def session_id() -> str:
39
+ global _SESSION, _SESSION_PID
40
+ if os.getpid() != _SESSION_PID:
41
+ _SESSION, _SESSION_PID = uuid.uuid4().hex, os.getpid()
37
42
  return _SESSION
38
43
 
39
44
 
@@ -81,6 +86,9 @@ def record(
81
86
  feature: Optional[str] = None,
82
87
  tool: Optional[str] = None,
83
88
  repo_size: Optional[str] = None,
89
+ outcome: Optional[str] = None,
90
+ execution_id: str = "",
91
+ parent_execution_id: str = "",
84
92
  ) -> None:
85
93
  """Record a telemetry event. Fire-and-forget — never blocks or raises.
86
94
 
@@ -91,7 +99,17 @@ def record(
91
99
  return
92
100
 
93
101
  try:
102
+ from sourcecode.telemetry.lifecycle import current
103
+ scope = current.get()
104
+ install = get_install_id()
105
+ if not install:
106
+ return
94
107
  ev = TelemetryEvent(
108
+ telemetry_context=telemetry_context(),
109
+ interface="mcp" if cmd == "mcp" or (scope and scope.interface == "mcp") else "cli",
110
+ outcome=outcome or ("succeeded" if success else "expected_error"),
111
+ execution_id=execution_id or (scope.id if scope else str(uuid.uuid4())),
112
+ parent_execution_id=parent_execution_id or (scope.parent if scope else ""),
95
113
  event=event,
96
114
  v=_sourcecode_version(),
97
115
  py=_python_version(),
@@ -111,11 +129,12 @@ def record(
111
129
  error_kind=error_kind,
112
130
  feature=feature,
113
131
  tool=tool,
114
- install=get_install_id(),
115
- session=_SESSION,
132
+ install=install,
133
+ session=session_id(),
116
134
  )
117
135
  payload = sanitize(ev)
118
- send(payload)
136
+ if payload.get("install"):
137
+ send(payload)
119
138
  except Exception:
120
139
  pass # telemetry must never affect the main process
121
140
 
@@ -17,6 +17,9 @@ from __future__ import annotations
17
17
 
18
18
  import json
19
19
  import os
20
+ import sys
21
+ import tempfile
22
+ import uuid
20
23
  from pathlib import Path
21
24
  from typing import Any
22
25
 
@@ -59,6 +62,8 @@ def is_enabled() -> bool:
59
62
  3. config file 'enabled' flag, if the user has made an explicit choice
60
63
  4. default: True
61
64
  """
65
+ if telemetry_context() != "production":
66
+ return False
62
67
  env = os.environ.get(_ENV_VAR, "").strip()
63
68
  if env == "0":
64
69
  return False
@@ -92,27 +97,58 @@ def mark_asked() -> None:
92
97
  _save(data)
93
98
 
94
99
 
100
+ def telemetry_context() -> str:
101
+ """Origin evidence only; never infer automation from MCP or usage volume."""
102
+ if "pytest" in sys.modules or os.environ.get("PYTEST_CURRENT_TEST"):
103
+ return "test"
104
+ if any(os.environ.get(k, "").lower() not in ("", "0", "false") for k in
105
+ ("CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "TF_BUILD", "JENKINS_URL", "BUILDKITE", "CIRCLECI", "TRAVIS", "TEAMCITY_VERSION", "DRONE", "SEMAPHORE")):
106
+ return "ci"
107
+ explicit = os.environ.get("SOURCECODE_TELEMETRY_CONTEXT", "")
108
+ if explicit in {"test", "ci", "development"}:
109
+ return explicit
110
+ # An editable source checkout is development, irrespective of working directory.
111
+ if (Path(__file__).resolve().parents[3] / ".git").exists():
112
+ return "development"
113
+ return "production"
114
+
115
+
95
116
  def get_install_id() -> str:
96
- """Stable anonymous install id — a random UUID v4.
117
+ """One random UUID per persistent config directory, shared across versions.
97
118
 
98
- Created lazily on first enabled event. NOT derived from hardware, email,
99
- hostname or any identifier — it only says "the same install across runs",
100
- which is what enables unique-user, conversion and retention metrics.
101
- Returns "" if it cannot be persisted (telemetry then degrades to events
102
- without a stable id, never an error).
119
+ A complete temporary file is published with an atomic, no-replace hard link.
120
+ Competing processes read the winner; settings writes cannot rotate identity.
121
+ Preserve a valid legacy JSON UUID. On unavailable storage, suppress telemetry.
103
122
  """
104
- data = _load()
105
- tel = data.setdefault("telemetry", {})
106
- iid = tel.get("install_id")
107
- if not iid:
108
- import uuid
109
- iid = str(uuid.uuid4())
110
- tel["install_id"] = iid
111
- _save(data)
112
- # If the write failed, re-read to avoid handing out a non-persisted id
113
- if not _load().get("telemetry", {}).get("install_id"):
114
- return ""
115
- return str(iid)
123
+ identity = _CONFIG_FILE.with_name("install-id")
124
+ temporary = None
125
+ try:
126
+ if identity.exists():
127
+ return str(uuid.UUID(identity.read_text(encoding="ascii").strip()))
128
+ legacy = _load().get("telemetry", {}).get("install_id")
129
+ try:
130
+ candidate = str(uuid.UUID(legacy)) if legacy else str(uuid.uuid4())
131
+ except (ValueError, TypeError, AttributeError):
132
+ candidate = str(uuid.uuid4())
133
+ identity.parent.mkdir(parents=True, exist_ok=True)
134
+ fd, temporary = tempfile.mkstemp(prefix=".install-", dir=identity.parent)
135
+ with os.fdopen(fd, "w", encoding="ascii") as stream:
136
+ stream.write(candidate)
137
+ stream.flush()
138
+ os.fsync(stream.fileno())
139
+ try:
140
+ os.link(temporary, identity)
141
+ except FileExistsError:
142
+ pass
143
+ return str(uuid.UUID(identity.read_text(encoding="ascii").strip()))
144
+ except (OSError, ValueError, TypeError, AttributeError):
145
+ return ""
146
+ finally:
147
+ if temporary:
148
+ try:
149
+ os.unlink(temporary)
150
+ except OSError:
151
+ pass
116
152
 
117
153
 
118
154
  def config_file_path() -> Path:
@@ -69,6 +69,12 @@ class TelemetryEvent:
69
69
  """
70
70
 
71
71
  event: str
72
+ schema_version: int = 2
73
+ telemetry_context: str = "production"
74
+ interface: str = "cli"
75
+ outcome: str = "succeeded"
76
+ execution_id: str = ""
77
+ parent_execution_id: str = ""
72
78
  ts: str = field(default_factory=_now_utc)
73
79
  v: str = ""
74
80
  py: str = ""
@@ -64,16 +64,18 @@ _SAFE_COMMANDS: frozenset[str] = frozenset({
64
64
  "audit-report", "migrate-recipe", "data-exposure", "posture", "migrate-check", "impact-chain",
65
65
  "pr-impact", "explain", "onboard", "review-pr", "fix-bug", "modernize", "rename-class",
66
66
  "chunk-file", "activate", "selftest", "explain-endpoint", "regress", "version", "schema",
67
- "config", "archetype", "cold-start",
67
+ "config", "archetype", "cold-start", "pack", "audit-verify",
68
68
  })
69
69
  _SAFE_EVENTS: frozenset[str] = frozenset({
70
70
  "command_executed",
71
+ "execution_started",
71
72
  "execution_completed",
72
73
  "execution_failed",
73
74
  "telemetry_enabled",
74
75
  "telemetry_disabled",
75
76
  "gate_blocked",
76
77
  "activation",
78
+ "activation_started",
77
79
  "mcp_tool_invoked",
78
80
  })
79
81
  # Closed set of gated features / task names. Used to learn which capability
@@ -112,7 +114,7 @@ def _safe_error_kind(value: str | None) -> str | None:
112
114
  if not value:
113
115
  return None
114
116
  # Exception class names are CamelCase identifiers, no spaces, no paths
115
- name = value.split(":")[-1].strip() # strip message after ':'
117
+ name = value.split(":", 1)[0].strip() # strip message after ':'
116
118
  name = name.split(".")[-1].strip() # strip module prefix
117
119
  if len(name) > 64 or _SPACE_PATTERN.search(name) or _PATH_PATTERN.search(name):
118
120
  return "UnknownError"
@@ -135,7 +137,7 @@ def _safe_tool(value: str | None) -> str | None:
135
137
 
136
138
  def _safe_session(value: str) -> str:
137
139
  """Session ID must be a short hex string only."""
138
- if re.match(r"^[0-9a-f]{1,16}$", value):
140
+ if re.match(r"^[0-9a-f]{1,32}$", value):
139
141
  return value
140
142
  return ""
141
143
 
@@ -155,6 +157,14 @@ def sanitize(event: TelemetryEvent) -> dict[str, Any]:
155
157
  If any field cannot be validated as safe, it is replaced with a safe default.
156
158
  """
157
159
  safe: dict[str, Any] = {
160
+ "domain": "licensing" if event.event in {"activation", "activation_started", "gate_blocked"} or event.command in {"activate", "auth"} else "execution",
161
+ "schema_version": 2,
162
+ "telemetry_context": _safe_str(event.telemetry_context, frozenset({"production", "development", "test", "ci"}), "unknown"),
163
+ "interface": _safe_str(event.interface, frozenset({"cli", "mcp"}), "unknown"),
164
+ "outcome": _safe_str(event.outcome, frozenset({"started", "succeeded", "negative", "expected_error", "internal_error", "unknown_error", "cancelled"}), "unknown"),
165
+ "execution_id": _safe_install(event.execution_id),
166
+ "parent_execution_id": _safe_install(event.parent_execution_id),
167
+ "capabilities": sorted({name for flag, name in {"--since": "incremental", "--compact": "compact", "--git-context": "git_context", "--agent": "agent"}.items() if flag in event.flags}),
158
168
  "event": _safe_str(event.event, _SAFE_EVENTS, "command_executed"),
159
169
  "ts": event.ts[:20] if event.ts else "", # truncate to date+time, no sub-second
160
170
  "v": event.v[:16] if event.v else "",
@@ -167,7 +177,7 @@ def sanitize(event: TelemetryEvent) -> dict[str, Any]:
167
177
  "output_fmt": _safe_str(event.output_fmt, _SAFE_FMTS, "json"),
168
178
  "repo_size": _safe_str(event.repo_size, _SAFE_SIZES, "unknown"),
169
179
  "duration": _safe_str(event.duration, _SAFE_DURATIONS, "unknown"),
170
- "success": bool(event.success),
180
+ "success": None if event.outcome == "started" else event.outcome in {"succeeded", "negative"},
171
181
  }
172
182
 
173
183
  if event.error_kind:
@@ -0,0 +1,30 @@
1
+ """Execution correlation; context local to a CLI invocation or async MCP call."""
2
+ from __future__ import annotations
3
+
4
+ from contextvars import ContextVar
5
+ from dataclasses import dataclass, field
6
+ import uuid
7
+
8
+
9
+ @dataclass
10
+ class Execution:
11
+ interface: str = "cli"
12
+ parent: str = ""
13
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
14
+ outcome: str = "succeeded"
15
+ command: str = "unknown"
16
+
17
+
18
+ current: ContextVar[Execution | None] = ContextVar("telemetry_execution", default=None)
19
+
20
+
21
+ def mark(outcome: str) -> None:
22
+ scope = current.get()
23
+ if scope is not None:
24
+ scope.outcome = outcome
25
+
26
+
27
+ def exit_outcome(code: object, scope: Execution) -> str:
28
+ if scope.outcome != "succeeded":
29
+ return scope.outcome
30
+ return "succeeded" if code is None or code == 0 else "unknown_error"
@@ -10,11 +10,29 @@ Design principles:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import atexit
14
+ import time
13
15
  import json
14
16
  import os
15
17
  import threading
16
18
  from typing import Any
17
19
 
20
+ from sourcecode.telemetry.config import telemetry_context
21
+
22
+ _pending: set[threading.Thread] = set()
23
+ _lock = threading.Lock()
24
+
25
+ def flush(timeout: float = 0.3) -> None:
26
+ """Best-effort bounded process-exit drain; never claim reliable delivery."""
27
+ deadline = time.monotonic() + timeout
28
+ with _lock:
29
+ pending = list(_pending)
30
+ for thread in pending:
31
+ thread.join(max(0, deadline - time.monotonic()))
32
+
33
+
34
+ atexit.register(flush)
35
+
18
36
  _DEFAULT_ENDPOINT = "https://qkndlmyekvujjdgthtmz.supabase.co/functions/v1/telemetry"
19
37
  _TIMEOUT_S = 3
20
38
 
@@ -28,6 +46,8 @@ def _endpoint() -> str:
28
46
 
29
47
  def _send_blocking(payload: dict[str, Any]) -> None:
30
48
  """Blocking send — runs inside a daemon thread only."""
49
+ if telemetry_context() != "production":
50
+ return
31
51
  try:
32
52
  import urllib.request
33
53
 
@@ -41,7 +61,8 @@ def _send_blocking(payload: dict[str, Any]) -> None:
41
61
  },
42
62
  method="POST",
43
63
  )
44
- urllib.request.urlopen(req, timeout=_TIMEOUT_S)
64
+ with urllib.request.urlopen(req, timeout=_TIMEOUT_S):
65
+ pass
45
66
  except Exception:
46
67
  pass # always silent — a missed event is not an error
47
68
 
@@ -51,5 +72,19 @@ def send(payload: dict[str, Any]) -> None:
51
72
 
52
73
  Returns immediately. The main process can exit without waiting.
53
74
  """
54
- t = threading.Thread(target=_send_blocking, args=(payload,), daemon=True)
55
- t.start()
75
+ if telemetry_context() != "production":
76
+ return
77
+
78
+ def deliver() -> None:
79
+ try:
80
+ _send_blocking(payload)
81
+ finally:
82
+ with _lock:
83
+ _pending.discard(threading.current_thread())
84
+
85
+ with _lock:
86
+ if len(_pending) >= 64:
87
+ return
88
+ t = threading.Thread(target=deliver, daemon=True)
89
+ _pending.add(t)
90
+ t.start()
sourcecode/update.py ADDED
@@ -0,0 +1,155 @@
1
+ """Safe, explicit ASK update checks and installation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import random
7
+ from pathlib import Path
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ import urllib.error
13
+ import urllib.request
14
+ from typing import Any
15
+
16
+ PYPI_URL = "https://pypi.org/pypi/sourcecode/json"
17
+ _CACHE_TTL = 24 * 60 * 60
18
+ _VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$")
19
+
20
+
21
+ def _version_tuple(value: str) -> tuple[int, int, int] | None:
22
+ match = _VERSION_RE.match(value.strip())
23
+ return tuple(int(part) for part in match.groups()[:3]) if match else None
24
+
25
+
26
+ def _cache_path() -> Path:
27
+ override = os.environ.get("SOURCECODE_UPDATE_CACHE")
28
+ return Path(override).expanduser() if override else Path.home() / ".cache" / "sourcecode" / "update-check.json"
29
+
30
+
31
+ def _runtime_allowed() -> bool:
32
+ from sourcecode.telemetry.config import telemetry_context
33
+ return telemetry_context() == "production" and os.environ.get("SOURCECODE_UPDATE_DISABLED") not in {"1", "true", "yes"}
34
+
35
+
36
+ def _lock_path() -> Path:
37
+ return _cache_path().with_name("update.lock")
38
+
39
+
40
+ def _acquire_lock(timeout: float = 2.0) -> int | None:
41
+ try:
42
+ _lock_path().parent.mkdir(parents=True, exist_ok=True)
43
+ except OSError:
44
+ return None
45
+ deadline = time.monotonic() + timeout
46
+ while time.monotonic() < deadline:
47
+ try:
48
+ return os.open(_lock_path(), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
49
+ except FileExistsError:
50
+ time.sleep(0.03 + random.random() * 0.04)
51
+ except OSError:
52
+ return None
53
+ return None
54
+
55
+
56
+ def _release_lock(fd: int | None) -> None:
57
+ if fd is None:
58
+ return
59
+ try:
60
+ os.close(fd)
61
+ _lock_path().unlink()
62
+ except OSError:
63
+ pass
64
+
65
+
66
+ def _read_cache() -> dict[str, Any] | None:
67
+ try:
68
+ data = json.loads(_cache_path().read_text(encoding="utf-8"))
69
+ if time.time() - float(data.get("checked_at", 0)) < _CACHE_TTL:
70
+ return data
71
+ except (OSError, ValueError, TypeError):
72
+ pass
73
+ return None
74
+
75
+
76
+ def _write_cache(data: dict[str, Any]) -> None:
77
+ try:
78
+ path = _cache_path()
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ temporary = path.with_suffix(".tmp")
81
+ temporary.write_text(json.dumps(data), encoding="utf-8")
82
+ temporary.replace(path)
83
+ except OSError:
84
+ pass
85
+
86
+
87
+ def current_version() -> str:
88
+ from sourcecode import __version__
89
+ return str(__version__)
90
+
91
+
92
+ def check(*, force: bool = False) -> dict[str, Any]:
93
+ """Return update status without raising or installing."""
94
+ current = current_version()
95
+ if not _runtime_allowed():
96
+ return {"status": "suppressed", "current_version": current, "reason": "non-production runtime"}
97
+ if not force:
98
+ cached = _read_cache()
99
+ if cached:
100
+ cached["cached"] = True
101
+ return cached
102
+ request = urllib.request.Request(PYPI_URL, headers={"Accept": "application/json", "User-Agent": f"sourcecode/{current}"})
103
+ try:
104
+ with urllib.request.urlopen(request, timeout=3) as response:
105
+ payload = json.loads(response.read(256 * 1024).decode("utf-8"))
106
+ latest = str(payload["info"]["version"])
107
+ if _version_tuple(latest) is None:
108
+ raise ValueError("invalid version from PyPI")
109
+ status = "available" if _version_tuple(latest) > _version_tuple(current) else "current"
110
+ result = {"status": status, "current_version": current, "latest_version": latest, "checked_at": time.time(), "source": "pypi"}
111
+ _write_cache(result)
112
+ return result
113
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
114
+ return {"status": "unavailable", "current_version": current, "reason": type(exc).__name__}
115
+
116
+
117
+ def install(target: str | None = None) -> dict[str, Any]:
118
+ """Install the latest package through the running interpreter's pip."""
119
+ if not _runtime_allowed():
120
+ return {"status": "suppressed", "reason": "non-production runtime", "current_version": current_version()}
121
+ previous = current_version()
122
+ status = check()
123
+ latest = target or status.get("latest_version")
124
+ if status.get("status") != "available" or not latest:
125
+ return {**status, "status": "current" if status.get("status") == "current" else status.get("status", "unavailable")}
126
+ target_tuple = _version_tuple(str(latest))
127
+ if target_tuple is None:
128
+ return {"status": "failed", "target_version": latest, "reason": "invalid_target_version"}
129
+ fd = _acquire_lock()
130
+ if fd is None:
131
+ return {"status": "busy", "target_version": latest, "reason": "another_update_in_progress"}
132
+ command = [sys.executable, "-m", "pip", "install", "--upgrade", "--disable-pip-version-check", "sourcecode"]
133
+ try:
134
+ result = subprocess.run(command, check=False, capture_output=True, text=True, timeout=120)
135
+ except (OSError, subprocess.SubprocessError) as exc:
136
+ result = None
137
+ failure = type(exc).__name__
138
+ else:
139
+ failure = "pip_exit" if result.returncode != 0 else None
140
+ try:
141
+ if failure:
142
+ rollback = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", f"sourcecode=={previous}"], check=False, capture_output=True, text=True, timeout=120)
143
+ return {"status": "failed", "target_version": latest, "reason": failure, "exit_code": getattr(result, "returncode", None), "rollback": "restored" if rollback.returncode == 0 else "failed"}
144
+ try:
145
+ from importlib.metadata import version as installed_version
146
+ observed = installed_version("sourcecode")
147
+ except Exception:
148
+ observed = ""
149
+ observed_tuple = _version_tuple(observed)
150
+ if observed_tuple is None or observed_tuple < target_tuple:
151
+ rollback = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", f"sourcecode=={previous}"], check=False, capture_output=True, text=True, timeout=120)
152
+ return {"status": "failed", "target_version": latest, "previous_version": previous, "loaded_version": observed, "reason": "installed_version_mismatch", "rollback": "restored" if rollback.returncode == 0 else "failed"}
153
+ return {"status": "updated", "previous_version": previous, "target_version": latest, "installed_version": observed, "restart_required": True}
154
+ finally:
155
+ _release_lock(fd)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.9.30
3
+ Version: 5.9.32
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
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
  > question. All analysis is local and deterministic; see [Privacy](#configuration--privacy)
60
60
  > before inspecting a repository whose telemetry policy you need to control.
61
61
 
62
- ![Version](https://img.shields.io/badge/version-5.9.30-blue)
62
+ ![Version](https://img.shields.io/badge/version-5.9.32-blue)
63
63
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
64
64
 
65
65
  Copyright (c) 2026 Dominique Haroun. All rights reserved. ASK Engine 5.7.0 and
@@ -290,7 +290,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
290
290
  # pip / pipx
291
291
  pipx install sourcecode # or: pip install sourcecode
292
292
 
293
- ask version # ask 5.9.30 — and, on a build that has aged,
293
+ ask version # ask 5.9.32 — and, on a build that has aged,
294
294
  # how many releases have probably shipped since
295
295
  ```
296
296
 
@@ -1,6 +1,6 @@
1
- sourcecode/__init__.py,sha256=iIGdq5ylassCH6DK0Z33Zccuye8sWa4451bkdU2VfXY,309
1
+ sourcecode/__init__.py,sha256=ncGdSArOW-Az9AQcz2GdsqINaoj8Lgl5TWfe6oqyjzc,309
2
2
  sourcecode/__main__.py,sha256=qDFVkobwsdZrX0B3stiRqsUNivVpybKO-egzAwCW4xw,165
3
- sourcecode/_build_commit.py,sha256=3Qaulg-_zvS_yOtxaeqUjXORG2nlJqhxh5-0wwNGM_A,12816
3
+ sourcecode/_build_commit.py,sha256=9ZnAT__s9q1eWX_Ez3hNstlJg_IEgCuX1j4sV4TBADk,12816
4
4
  sourcecode/access_drift.py,sha256=uYD8ipiEAaTnkz0IhG6ujU_KI3LghdY-6ycurs-BZnc,9355
5
5
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
6
6
  sourcecode/analysis_budget.py,sha256=ZuNEboPF8A6GK-_V7WNt92ezgriga7_Rbr-vvSN4UEE,4183
@@ -35,7 +35,7 @@ sourcecode/change_plan.py,sha256=drAoDsqFplutCLc6Y0xue7sYkTls0SpYh7aOXvttv3I,129
35
35
  sourcecode/ci_output.py,sha256=X4NRokWWMRF9Ex8OQrEO2g04GzJ00GDP5zYIoqI9YCc,4625
36
36
  sourcecode/cir_graphs.py,sha256=RytqSv1WF8kdlfXj2fPQz16LY4akS2kyThbdWL8beCQ,14835
37
37
  sourcecode/classifier.py,sha256=ft1RGPIChvO84q2QMrEH93x1J15AXNWMh26lnxvoRao,26476
38
- sourcecode/cli.py,sha256=NOhZomxiYik0FuqI2_ttsPsiNYO9xuODz3ScJMNIBe0,988101
38
+ sourcecode/cli.py,sha256=5NIrvizSSIb966DIpoELNqy_nVO94B5QMSKMGfupfj4,994167
39
39
  sourcecode/click_runtime.py,sha256=K2cUdzFhbd6uEUpucWptXE89_z-ZNZGsqgd-34usUXk,1890
40
40
  sourcecode/client_calls.py,sha256=YOPnfRZzRyqmdHvXSVeiunW8cuiuCuWXacNIQRgKMCw,22640
41
41
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
@@ -101,7 +101,7 @@ sourcecode/integration_coordinates.py,sha256=rvuApnS7WdRUbeQLg_oKj-Gi0koO2RKdw6H
101
101
  sourcecode/intent_router.py,sha256=hC_o0VYfSjzSXecFkpiD7vzFgvt44YAw6E_jrMeGiZ4,15555
102
102
  sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
103
103
  sourcecode/jvm_inventory.py,sha256=06mlOWSwfs9jBk4lGkCSZHRbapOG41dQ5gopvOVf7dE,14182
104
- sourcecode/license.py,sha256=eIEuIVQXywl_S9LOIACOEpJ4M5RyaPvWrqg_jSNOrXA,31088
104
+ sourcecode/license.py,sha256=HJ-RBnh7hRIhzoABr-gGstgdbsRVmv_lg5Vh_aZK0oA,31164
105
105
  sourcecode/manifest_cache.py,sha256=mhCgOYNQgn1VIQ2HUY57Dmer1AIF-1suR0XddnI9M2g,4381
106
106
  sourcecode/mcp_nudge.py,sha256=beNjfyb7z09T1rnS4t6Q4qfbLzZQNaP6ddCA4XpmV0s,3256
107
107
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
@@ -143,7 +143,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
143
143
  sourcecode/reference_facts.py,sha256=mx7H-Ax5y8sKAQHrnJCXlgjkUzqZtQTByv9Zy2lxKQQ,15500
144
144
  sourcecode/regress.py,sha256=6rlFP2rsIzVKAA0spJ66To528GyBtnRI2Zg9vAGq8kQ,13264
145
145
  sourcecode/regulatory_pack.py,sha256=xVWd8hYg6MjGfkFP8UWcYeLmNMxEk8k2uSbTcnGMbhg,3421
146
- sourcecode/release_info.py,sha256=vYe0WiVq1H7D5h07cvTfNRXi_43pJK3SC7lcaTQG6pc,5918
146
+ sourcecode/release_info.py,sha256=u-opkkqIN4jnbtXmQVPV35Ga9zvbORXP2w2MHNUbbDg,5918
147
147
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
148
148
  sourcecode/remediation_diff.py,sha256=fGYqPmQF7bO54L8ivztX5PgmW4fVu54gNcG6OEQ192U,9730
149
149
  sourcecode/remediation_flow.py,sha256=OFlrNCD2dKw1WlX5cHqVl1yrQZHi2TjCThH8Q3X1PwI,10294
@@ -200,6 +200,7 @@ sourcecode/timeline_cache.py,sha256=99RQ-MW3wqxjtuHcVB_k83USsxWP6DqzeupfSzKR_CE,
200
200
  sourcecode/token_estimate.py,sha256=_2cworfPkfP2ZxVcwX_Ltu0vFdMvK-FMuTY3qJAqI5M,9415
201
201
  sourcecode/tree_utils.py,sha256=oAwkGyrixO2VMX4ZMIDzg9V2oBxt3KIxFN3ahVMxKJo,2174
202
202
  sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
203
+ sourcecode/update.py,sha256=D562zph5vPm7FTVjeBeF6xtZwy4ByT31SN6A-Uumk44,6440
203
204
  sourcecode/validation_inference.py,sha256=-oWJqE6PqkqcZbCFDJeWCQHgI9Dbuv5ggJWD3LE_2IU,20817
204
205
  sourcecode/validation_surface.py,sha256=LeoOeZfTZmz5l4f2EDXNlOlzoTlL3EMVrYY8HEKGEg0,36716
205
206
  sourcecode/verify_edit.py,sha256=Ha0JNsC6CYHjtbetVZurLetqBNjriykYJ8ITDUMuNVY,63479
@@ -240,8 +241,8 @@ sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,17
240
241
  sourcecode/mcp/dependency_import.py,sha256=wfvw1rmDE3n2mHDj-yHnCuy9HZ7TtPqsHG2BU-Scl1k,4146
241
242
  sourcecode/mcp/orchestrator.py,sha256=RUzx2uRfwk-rnEy1RKeXjTBPfUaQiBldo-b2FryL6yI,36826
242
243
  sourcecode/mcp/registry.py,sha256=xewjyZDr2A8Fdu1SUOT1QXJ86mww-pbqL4EFpXuRklo,69393
243
- sourcecode/mcp/runner.py,sha256=dRvI0xS2wa8DH0diYjJiAoaGeVdtf389PekWwwh8wRI,3083
244
- sourcecode/mcp/server.py,sha256=L3xxLaG-bLe3Asr4ZESiEIYldhvKHkfpCMoucndvLvY,76070
244
+ sourcecode/mcp/runner.py,sha256=5cvdsR04EWg9HTo8SNIytJA-OVE01nUYZUO-3ZbItb0,3352
245
+ sourcecode/mcp/server.py,sha256=uzRLjOHZR_fm1ZxhINcljG8_YvuI37KD1l5p_y_ZnjQ,78323
245
246
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
246
247
  sourcecode/mcp/onboarding/applier.py,sha256=Sx9vHTaXr_M1Bs4uMjO7qa_a2_p4J9oz93oJsKQ7ds4,3298
247
248
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -272,19 +273,20 @@ sourcecode/schemas/envelope-v1.schema.json,sha256=JJ4pAS8jZCYRp9uOoCbHkpUUpotVMI
272
273
  sourcecode/schemas/pr-impact-v1.schema.json,sha256=fC4DLqY--N5iJpm5wCxbUeILianYbhNDNYV3n4D5gFg,3756
273
274
  sourcecode/schemas/verify-edit-v1.schema.json,sha256=0jp-yoFIdmfq-4IrzrOPCAsEN7JcKg6cjq7-87_Ez0U,3537
274
275
  sourcecode/schemas/verify-v1.schema.json,sha256=rY1h_ub3hMAKCHO6WmnyKDtI4b37xLAEC6uuoEhKR64,3441
275
- sourcecode/telemetry/__init__.py,sha256=5jg6mIrhnt59FgxQsQcihxkHCWM07UFQFcuGwMPa9v8,3489
276
- sourcecode/telemetry/config.py,sha256=QPuTK-oc-Bg_EQftyFxLHPSnLRiZ5NegWYfX4BcD8iI,3842
276
+ sourcecode/telemetry/__init__.py,sha256=9WpZjzY2HA3tasgsRep5v5dFzEWhZ8A8bYiY1P8GviI,4372
277
+ sourcecode/telemetry/config.py,sha256=ZnF4FkluBAeTlSaivp-qsBoQe011HHsjH_DJw9CrqVI,5443
277
278
  sourcecode/telemetry/consent.py,sha256=-Wb8BFGXEexOs8M0Eja7F-T6ZtLNPeRoZnHcHvlP7-Q,2153
278
- sourcecode/telemetry/events.py,sha256=l3Z2SQUO4_C2D2ItXOOdVNimAmHedUHnUxqYFTxnsAE,2900
279
- sourcecode/telemetry/filters.py,sha256=-lqT2oorECuqEIdeHb--7AZ6BCaUx0bya720a-Z5x6w,7335
280
- sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
279
+ sourcecode/telemetry/events.py,sha256=_ZBAo_U4UeW1GsnzHdPnVA8RDAz6U-pNPyQ3zfhaB3g,3089
280
+ sourcecode/telemetry/filters.py,sha256=_3fid41dDWMdkM-2XzzzHVeUqD72nKPV9wQfjOUatEc,8378
281
+ sourcecode/telemetry/lifecycle.py,sha256=QspRt5LfDLzAvgqmaxYAe04PZbBGIPsiPtsByKOlYuc,813
282
+ sourcecode/telemetry/transport.py,sha256=BVjvRd1IrxQySl2XpT_T4abkodqOIjSULXcYLDMQriE,2551
281
283
  sourcecode/_docs/DEFECT-LEDGER.md,sha256=o2CY-nisHJOuYpn2qnUv2jVjEtSdnkiC-ilnu0kiFss,1233466
282
284
  sourcecode/_docs/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
283
285
  sourcecode/_docs/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
284
- sourcecode/_docs/USER_GUIDE.md,sha256=6HvB261G_xfmGmG-LR9lEre4wa1BrKUpQyLrLWT-n48,108622
285
- sourcecode-5.9.30.dist-info/METADATA,sha256=0gfJtD6INOf2-mhESXct4YEif71nu7X4sDZt3jUPr8I,67653
286
- sourcecode-5.9.30.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
287
- sourcecode-5.9.30.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
288
- sourcecode-5.9.30.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
289
- sourcecode-5.9.30.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
290
- sourcecode-5.9.30.dist-info/RECORD,,
286
+ sourcecode/_docs/USER_GUIDE.md,sha256=fA3xYbXNM8_6be5c1Mi0qGw929TVvbbD9Hadl8dmUJ0,108622
287
+ sourcecode-5.9.32.dist-info/METADATA,sha256=CTJuEtjpFyPm6AGLe4RS-V4zs5fn5DssD8sWPFlEcHU,67653
288
+ sourcecode-5.9.32.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
289
+ sourcecode-5.9.32.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
290
+ sourcecode-5.9.32.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
291
+ sourcecode-5.9.32.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
292
+ sourcecode-5.9.32.dist-info/RECORD,,