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 +1 -1
- sourcecode/_build_commit.py +1 -1
- sourcecode/_docs/USER_GUIDE.md +1 -1
- sourcecode/cli.py +126 -18
- sourcecode/license.py +1 -0
- sourcecode/mcp/runner.py +10 -2
- sourcecode/mcp/server.py +47 -2
- sourcecode/release_info.py +1 -1
- sourcecode/telemetry/__init__.py +24 -5
- sourcecode/telemetry/config.py +54 -18
- sourcecode/telemetry/events.py +6 -0
- sourcecode/telemetry/filters.py +14 -4
- sourcecode/telemetry/lifecycle.py +30 -0
- sourcecode/telemetry/transport.py +38 -3
- sourcecode/update.py +155 -0
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/METADATA +3 -3
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/RECORD +21 -19
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/WHEEL +0 -0
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.9.30.dist-info → sourcecode-5.9.32.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/_build_commit.py
CHANGED
|
@@ -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 = '
|
|
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}
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
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
|
-
|
|
4940
|
+
ok = scope.outcome in {"succeeded", "negative"}
|
|
4908
4941
|
_tel.record(
|
|
4909
|
-
"execution_completed",
|
|
4910
|
-
|
|
4911
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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.
|
sourcecode/release_info.py
CHANGED
|
@@ -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-
|
|
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
|
sourcecode/telemetry/__init__.py
CHANGED
|
@@ -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
|
|
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=
|
|
115
|
-
session=
|
|
132
|
+
install=install,
|
|
133
|
+
session=session_id(),
|
|
116
134
|
)
|
|
117
135
|
payload = sanitize(ev)
|
|
118
|
-
|
|
136
|
+
if payload.get("install"):
|
|
137
|
+
send(payload)
|
|
119
138
|
except Exception:
|
|
120
139
|
pass # telemetry must never affect the main process
|
|
121
140
|
|
sourcecode/telemetry/config.py
CHANGED
|
@@ -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
|
-
"""
|
|
117
|
+
"""One random UUID per persistent config directory, shared across versions.
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
sourcecode/telemetry/events.py
CHANGED
|
@@ -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 = ""
|
sourcecode/telemetry/filters.py
CHANGED
|
@@ -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(":")[
|
|
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,
|
|
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":
|
|
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
|
-
|
|
55
|
-
|
|
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.
|
|
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
|
-

|
|
63
63
|

|
|
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.
|
|
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=
|
|
1
|
+
sourcecode/__init__.py,sha256=ncGdSArOW-Az9AQcz2GdsqINaoj8Lgl5TWfe6oqyjzc,309
|
|
2
2
|
sourcecode/__main__.py,sha256=qDFVkobwsdZrX0B3stiRqsUNivVpybKO-egzAwCW4xw,165
|
|
3
|
-
sourcecode/_build_commit.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
244
|
-
sourcecode/mcp/server.py,sha256=
|
|
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=
|
|
276
|
-
sourcecode/telemetry/config.py,sha256=
|
|
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=
|
|
279
|
-
sourcecode/telemetry/filters.py,sha256
|
|
280
|
-
sourcecode/telemetry/
|
|
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=
|
|
285
|
-
sourcecode-5.9.
|
|
286
|
-
sourcecode-5.9.
|
|
287
|
-
sourcecode-5.9.
|
|
288
|
-
sourcecode-5.9.
|
|
289
|
-
sourcecode-5.9.
|
|
290
|
-
sourcecode-5.9.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|