sourcecode 2.6.3__tar.gz → 2.6.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {sourcecode-2.6.3 → sourcecode-2.6.10}/CHANGELOG.md +177 -0
  2. {sourcecode-2.6.3 → sourcecode-2.6.10}/PKG-INFO +1 -1
  3. {sourcecode-2.6.3 → sourcecode-2.6.10}/pyproject.toml +1 -1
  4. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/cli.py +162 -14
  6. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/file_classifier.py +6 -2
  7. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/onboarding/applier.py +29 -15
  8. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/onboarding/detector.py +38 -0
  9. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/onboarding/planner.py +2 -2
  10. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/server.py +45 -6
  11. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp_nudge.py +3 -2
  12. sourcecode-2.6.10/src/sourcecode/readiness_timeline.py +178 -0
  13. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/repository_ir.py +15 -0
  14. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_impact.py +1 -1
  15. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_tx_analyzer.py +141 -4
  16. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/__init__.py +2 -0
  17. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/consent.py +2 -1
  18. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/events.py +3 -0
  19. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/filters.py +20 -1
  20. sourcecode-2.6.10/src/sourcecode/token_estimate.py +213 -0
  21. {sourcecode-2.6.3 → sourcecode-2.6.10}/.github/workflows/build-windows.yml +0 -0
  22. {sourcecode-2.6.3 → sourcecode-2.6.10}/.gitignore +0 -0
  23. {sourcecode-2.6.3 → sourcecode-2.6.10}/.ruff.toml +0 -0
  24. {sourcecode-2.6.3 → sourcecode-2.6.10}/CONTRIBUTING.md +0 -0
  25. {sourcecode-2.6.3 → sourcecode-2.6.10}/LICENSE +0 -0
  26. {sourcecode-2.6.3 → sourcecode-2.6.10}/README.md +0 -0
  27. {sourcecode-2.6.3 → sourcecode-2.6.10}/SECURITY.md +0 -0
  28. {sourcecode-2.6.3 → sourcecode-2.6.10}/raw +0 -0
  29. {sourcecode-2.6.3 → sourcecode-2.6.10}/scripts/compare_integration_engines.py +0 -0
  30. {sourcecode-2.6.3 → sourcecode-2.6.10}/scripts/customer_smoke_test.sh +0 -0
  31. {sourcecode-2.6.3 → sourcecode-2.6.10}/scripts/generate_jdk_exports.py +0 -0
  32. {sourcecode-2.6.3 → sourcecode-2.6.10}/scripts/perf_harness.py +0 -0
  33. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/adaptive_scanner.py +0 -0
  34. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/archetype.py +0 -0
  35. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/architectural_baseline.py +0 -0
  36. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/architectural_delta.py +0 -0
  37. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/architecture_analyzer.py +0 -0
  38. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/architecture_summary.py +0 -0
  39. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/ast_extractor.py +0 -0
  40. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/cache.py +0 -0
  41. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/call_surface.py +0 -0
  42. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/caller_metrics.py +0 -0
  43. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/canonical_ir.py +0 -0
  44. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/change_plan.py +0 -0
  45. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/cir_graphs.py +0 -0
  46. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/classifier.py +0 -0
  47. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/code_notes_analyzer.py +0 -0
  48. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/compare.py +0 -0
  49. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/confidence_analyzer.py +0 -0
  50. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/constraint_diff.py +0 -0
  51. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/context_cache.py +0 -0
  52. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/context_graph.py +0 -0
  53. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/context_scorer.py +0 -0
  54. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/context_summarizer.py +0 -0
  55. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/contract_diff.py +0 -0
  56. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/contract_model.py +0 -0
  57. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/contract_pipeline.py +0 -0
  58. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/coverage_parser.py +0 -0
  59. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/dependency_analyzer.py +0 -0
  60. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/__init__.py +0 -0
  61. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/base.py +0 -0
  62. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/csproj_parser.py +0 -0
  63. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/dart.py +0 -0
  64. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/dotnet.py +0 -0
  65. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/elixir.py +0 -0
  66. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/go.py +0 -0
  67. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/heuristic.py +0 -0
  68. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/hybrid.py +0 -0
  69. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/java.py +0 -0
  70. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/jvm_ext.py +0 -0
  71. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/nodejs.py +0 -0
  72. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/parsers.py +0 -0
  73. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/php.py +0 -0
  74. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/project.py +0 -0
  75. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/python.py +0 -0
  76. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/ruby.py +0 -0
  77. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/rust.py +0 -0
  78. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/systems.py +0 -0
  79. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/terraform.py +0 -0
  80. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/detectors/tooling.py +0 -0
  81. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/doc_analyzer.py +0 -0
  82. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/dynamic_argument_surface.py +0 -0
  83. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/endpoint_literals.py +0 -0
  84. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/endpoint_metrics.py +0 -0
  85. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/entrypoint_classifier.py +0 -0
  86. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/env_analyzer.py +0 -0
  87. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/error_schema.py +0 -0
  88. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/evidence_provider.py +0 -0
  89. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/explain.py +0 -0
  90. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/file_chunker.py +0 -0
  91. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/filter_surface.py +0 -0
  92. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/format_contract.py +0 -0
  93. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/fqn_utils.py +0 -0
  94. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/git_analyzer.py +0 -0
  95. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/graph_analyzer.py +0 -0
  96. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/graph_evidence.py +0 -0
  97. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/hibernate_strat.py +0 -0
  98. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/jdk_exports.py +0 -0
  99. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/license.py +0 -0
  100. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/__init__.py +0 -0
  101. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  102. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  103. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/orchestrator.py +0 -0
  104. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/registry.py +0 -0
  105. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/mcp/runner.py +0 -0
  106. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/metrics_analyzer.py +0 -0
  107. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/migrate_check.py +0 -0
  108. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/openapi_surface.py +0 -0
  109. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/output_budget.py +0 -0
  110. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/parse_cache.py +0 -0
  111. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/path_filters.py +0 -0
  112. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/perf.py +0 -0
  113. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/pr_comment_renderer.py +0 -0
  114. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/pr_impact.py +0 -0
  115. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/prepare_context.py +0 -0
  116. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/progress.py +0 -0
  117. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/ranking_engine.py +0 -0
  118. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/reconciliation.py +0 -0
  119. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/redactor.py +0 -0
  120. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/relevance_scorer.py +0 -0
  121. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/rename_refactor.py +0 -0
  122. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/repo_classifier.py +0 -0
  123. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/__init__.py +0 -0
  124. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/context.py +0 -0
  125. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/errors.py +0 -0
  126. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/executor.py +0 -0
  127. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/planner.py +0 -0
  128. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/query.py +0 -0
  129. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/request.py +0 -0
  130. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/resolution.py +0 -0
  131. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/result.py +0 -0
  132. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/retriever.py +0 -0
  133. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/runtime.py +0 -0
  134. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps.py +0 -0
  135. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
  136. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_graph.py +0 -0
  137. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_impact.py +0 -0
  138. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_intf.py +0 -0
  139. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_struct.py +0 -0
  140. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/retrieval/steps_txsec.py +0 -0
  141. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/ris.py +0 -0
  142. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/runtime_classifier.py +0 -0
  143. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/scanner.py +0 -0
  144. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/schema.py +0 -0
  145. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/security_config.py +0 -0
  146. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/security_posture.py +0 -0
  147. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/semantic_analyzer.py +0 -0
  148. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/semantic_impact_engine.py +0 -0
  149. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/semantic_integration_engine.py +0 -0
  150. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/semantic_services.py +0 -0
  151. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/serializer.py +0 -0
  152. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_event_topology.py +0 -0
  153. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_findings.py +0 -0
  154. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_model.py +0 -0
  155. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_security_audit.py +0 -0
  156. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/spring_semantic.py +0 -0
  157. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/summarizer.py +0 -0
  158. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/config.py +0 -0
  159. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/telemetry/transport.py +0 -0
  160. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/tree_utils.py +0 -0
  161. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/type_usage_surface.py +0 -0
  162. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/validation_inference.py +0 -0
  163. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/validation_surface.py +0 -0
  164. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/verify_edit.py +0 -0
  165. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/verify_rules.py +0 -0
  166. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/version_check.py +0 -0
  167. {sourcecode-2.6.3 → sourcecode-2.6.10}/src/sourcecode/workspace.py +0 -0
  168. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/functions/README.md +0 -0
  169. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/functions/get-license/index.ts +0 -0
  170. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  171. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/functions/telemetry/index.ts +0 -0
  172. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/sql/license_event_ordering.sql +0 -0
  173. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/sql/licensing_schema.sql +0 -0
  174. {sourcecode-2.6.3 → sourcecode-2.6.10}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,182 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.10] — 2026-07-25
4
+
5
+ **Identical runs now produce identical output.** Found by a field battery across twelve
6
+ real repositories.
7
+
8
+ ### Fixed
9
+
10
+ - **Annotation `evidence` had an unstable order** in the agent view's `file_relevance`:
11
+ the list came out of a frozenset, so its order depended on `PYTHONHASHSEED` and changed
12
+ between processes — the same repository analysed twice differed byte-for-byte while
13
+ being identical as data. The values were never wrong; the ordering defeated byte-level
14
+ diffing and reproducible caching. Long-standing, not introduced by a recent release.
15
+
16
+ ## [2.6.9] — 2026-07-25
17
+
18
+ **MCP tool usage is now counted, anonymously.** Which capabilities agents actually reach
19
+ for was invisible: MCP calls run the CLI in-process, so their events were
20
+ indistinguishable from direct CLI use.
21
+
22
+ ### Added
23
+
24
+ - **`mcp_tool_invoked` telemetry event** recording the invoked tool's own name
25
+ (`get_compact_context`, `get_impact_context`, …), whether it succeeded, and the existing
26
+ duration bucket. Counted once per call at the single dispatch point, on every exit path.
27
+
28
+ ### Notes
29
+
30
+ - The tool **arguments are never sent** — they carry repository paths — and neither is any
31
+ part of the result. The name is a fixed identifier from ASK Engine's own tool registry,
32
+ not user input, and the sanitization layer still shape-validates it before transmission.
33
+ - Same opt-out as every other event (`ask telemetry disable`, `SOURCECODE_TELEMETRY=0`,
34
+ `DO_NOT_TRACK=1`); with telemetry off, nothing is recorded. `docs/privacy.md` and the
35
+ first-run notice document the new field.
36
+
37
+ ## [2.6.8] — 2026-07-25
38
+
39
+ **One-click MCP setup now reaches Windsurf and VS Code.** `ask mcp init` / `status` /
40
+ `remove` cover four clients instead of two.
41
+
42
+ ### Added
43
+
44
+ - **Windsurf** (`--target windsurf`) and **VS Code** (`--target vscode`) as MCP onboarding
45
+ targets, detected from their standard per-platform config locations. VS Code keys its
46
+ servers under `servers` (not `mcpServers`) and receives an explicit `"type": "stdio"` on
47
+ the entry; the config shape is now a property of each client, so the other clients' files
48
+ are written exactly as before.
49
+
50
+ ### Fixed
51
+
52
+ - **`mcp status` reported a built-in server as `EXTERNAL`** for configs registered under the
53
+ canonical `ask` entry name: the drift check still looked up the deprecated `sourcecode`
54
+ key, under a hardcoded `mcpServers`. It now reads through the client's own key and accepts
55
+ either entry name.
56
+ - **The "no MCP clients found" hint printed the deprecated manual snippet** (`"sourcecode":
57
+ {"command": "sourcecode", ...}`). It now shows the canonical `ask` entry and notes VS
58
+ Code's different key.
59
+
60
+ ## [2.6.7] — 2026-07-25
61
+
62
+ **The context layer now reports what it saves: `_meta.token_economy`.** Responses used to
63
+ report only what ASK emits. They now also report the measured counterfactual — what reading
64
+ the same files raw would have cost the agent. Additive; no existing output changes.
65
+
66
+ ### Added
67
+
68
+ - **`_meta.token_economy` on the agent-facing responses** (`--compact`, `--agent`, and
69
+ `cold-start`, on both cache-hit and fresh paths; the MCP `get_compact_context` tool inherits
70
+ it). Fields: `emitted` (what this response costs), `baseline_raw_read` (what the agent would
71
+ have spent reading raw the files the response cites, plus the build manifests it derives
72
+ from), `saved`, `files_counted`, plus the declared `basis`, `token_model` and `estimate`
73
+ flag. Example on a small Spring repo: `--compact` emits ~1.1K tokens against a ~4.9K raw-read
74
+ baseline; `--agent` emits ~3.9K against ~16.3K over 22 cited files.
75
+ - **`sourcecode.token_estimate`** — `estimate_tokens()` and `token_economy()`, the single
76
+ source of the token model (previously an inline `size // 4` in `cold-start`).
77
+
78
+ ### Notes
79
+
80
+ - Honest by construction: the token model is `chars/4` — an approximation, declared on every
81
+ block, with no tokenizer dependency. A file counts toward the baseline only when the
82
+ response's own text resolves to a real file inside the repository, so fully-qualified names
83
+ and prose never inflate it; the baseline is the conservative cited-files-plus-manifests set,
84
+ never the whole repository; `saved` is clamped at zero; and the block is a pure function of
85
+ the response and the files on disk. It is an upper-bound estimate of tokens avoided, never a
86
+ monetary or ROI figure.
87
+ - No "hallucinations avoided" counter is emitted. It is not measurable, so it is not claimed;
88
+ what the product does guarantee — every claim carrying its `source_file` and FQN — is
89
+ already visible in the responses themselves.
90
+ - The cached view stored on disk never embeds the block; it is recomputed against the working
91
+ tree on every emission.
92
+
93
+ ## [2.6.6] — 2026-07-24
94
+
95
+ **Migration readiness as a time series: `migrate-check --snapshot` / `--trend`.** The
96
+ quantified, budgetable readiness figures can now be tracked across commits — the
97
+ days-remaining line a migration lead reports to management. Additive; no existing output
98
+ changes.
99
+
100
+ ### Added
101
+
102
+ - **`migrate-check --snapshot`** persists the run's budgetable figures — `readiness_score`,
103
+ `estimated_effort_days`, the per-dimension scores (jakarta / boot3 / hibernate / jdk),
104
+ and `blocking_count` — as a compact `readiness-snapshot-v1` artifact under `--history-dir`
105
+ (default `<repo>/.ask/readiness-history`). `--ref` labels the point (e.g. a sprint or
106
+ version). Capturing the same repo over successive commits builds the series.
107
+ - **`migrate-check --trend`** reads the stored series and reports first→last movement per
108
+ track (score and days-remaining over time), ordered by capture time. Honest by design:
109
+ movement only, never an improving/degrading label; no delta is emitted across a missing
110
+ endpoint (a `None` readiness yields no delta); and the `readiness_score` is flagged
111
+ `comparable: false` when the applicable dimension set changed between endpoints (the score
112
+ is a min over dimensions, so a different set is not strictly comparable). The projection is
113
+ deterministic for a fixed snapshot set.
114
+
115
+ ## [2.6.5] — 2026-07-24
116
+
117
+ **Mistyped-subcommand ergonomics: `ask` no longer swallows a typo'd command as a path
118
+ and then blames the wrong token.** CLI-only; no analysis behaviour changes.
119
+
120
+ ### Fixed
121
+
122
+ - **A mistyped command now names the token the user got wrong, and suggests the real
123
+ command.** `ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so
124
+ a misspelled subcommand used to be swallowed as a path and surface a misleading error
125
+ about the *next* token — `ask notacommand /tmp` complained `No such command '/tmp'` (the
126
+ path), and `ask spring-audi` complained the directory did not exist. Now:
127
+ - a first positional followed by another non-subcommand positional
128
+ (`ask notacommand /tmp`) is rejected as an unknown command (exit `2`), naming the
129
+ first token and — when it is close to a real command — suggesting it;
130
+ - a lone token that does not resolve to a directory but closely matches a command
131
+ (`ask spring-audi`) adds a *did you mean `ask spring-audit`* hint to the path error.
132
+
133
+ The implicit-scan contract is unchanged: a single positional is still treated as a
134
+ repository path with existence validated downstream, and the documented
135
+ `ask <subcommand> <path>` order (and the `ask <path> <subcommand>` rejection) are
136
+ untouched.
137
+
138
+ ## [2.6.4] — 2026-07-24
139
+
140
+ **TX-006: self-invocation proxy-bypass detection — a new Spring transaction rule that
141
+ also participates automatically in the `verify-edit` write-loop gate.** Plus a root-cause
142
+ false-positive fix in body-fact extraction and one internal-id output leak removed. No
143
+ change to any existing command's contract. Suite: **3742 passed, 5 skipped, 1 deselected**
144
+ (the deselected chunker test is a pre-existing external-fixture flake).
145
+
146
+ ### Added
147
+
148
+ - **TX-006 — self-invocation of a `@Transactional` sibling (Spring proxy bypass).** A
149
+ same-instance call — an unqualified `foo()` or an explicit `this.foo()` to a method of
150
+ the same class — does not pass through the Spring proxy, so the target's method-level
151
+ `@Transactional` is silently ignored at runtime (the Sonar S2229 class of bug). Detection
152
+ is structural and vendor-agnostic, reading the Semantic-IR `invocation` atoms already
153
+ produced by the pipeline: the receiver is `None`/`this` and the call resolves to an
154
+ unambiguous same-class sibling that carries a method-level `@Transactional`. False-positive
155
+ guards are built in — overloaded/ambiguous targets are skipped, class-level-only
156
+ `@Transactional` is not flagged (the proxy already covers the outer entry method), and the
157
+ legitimate self-injection escape hatch (`self.foo()` through an injected same-type field)
158
+ uses a field receiver and never resolves as a sibling. Severity is tiered: `high` only when
159
+ the callee propagation mandates a distinct boundary (`REQUIRES_NEW`/`NESTED`/`NOT_SUPPORTED`/
160
+ `NEVER`/`MANDATORY`); a plain `REQUIRED`/`SUPPORTS` callee or a `readOnly` getter is `medium`.
161
+ Surfaced by `ask spring-audit` and, with no additional wiring, by the `verify-edit`
162
+ `tx_boundary_changed` axis — an edit that *introduces* a self-invocation now breaks the
163
+ write-loop verdict before commit, while a pre-existing one never fails an unrelated change.
164
+
165
+ ### Fixed
166
+
167
+ - **Chained-call receivers no longer read as self-calls (body-fact extraction).** A
168
+ method-chained invocation such as `Context.getBean().method()` left the receiver empty
169
+ because the receiver is an expression, not a bare identifier — causing it to be mistaken
170
+ for an unqualified self-call. A callee immediately preceded by `.` is now marked as a
171
+ non-self expression receiver. This is a general sibling-resolution correctness fix
172
+ (it also prevents a chained sibling-named call from being mis-resolved to a call edge);
173
+ `body_facts` is not a `cir_hash` input, so the fix is hash-neutral. It removed roughly
174
+ half of the raw TX-006 candidates on a real Spring codebase — all of them true bypass
175
+ false-positives, including a service that correctly routes `REQUIRES_NEW` through a
176
+ service-locator proxy.
177
+ - **Internal bug id removed from user-facing output.** The self-referential-exclusion
178
+ warning in `impact` no longer prints the internal `(BUG #2)` tag; the explanation stays.
179
+
3
180
  ## [2.6.3] — 2026-07-24
4
181
 
5
182
  **`ask verify-edit` — the in-loop semantic diff gate lands (the read-context → write-loop
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.6.3
3
+ Version: 2.6.10
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "2.6.3"
7
+ version = "2.6.10"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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__ = "2.6.3"
7
+ __version__ = "2.6.10"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import difflib
5
6
  import os
6
7
  import sys
7
8
  import threading
@@ -331,6 +332,54 @@ def _reject_path_before_subcommand(path_token: str, subcommand: str) -> "NoRetur
331
332
  raise SystemExit(2)
332
333
 
333
334
 
335
+ def _reject_unknown_command(token: str) -> "NoReturn":
336
+ """Refuse a first positional that is neither a known subcommand nor a real path.
337
+
338
+ `ask` accepts a bare repository path (`ask ./repo`) as an implicit scan, so a
339
+ mistyped subcommand used to be swallowed as a path and then surfaced a
340
+ misleading error naming the *wrong* token — `ask spring-audi` complained the
341
+ directory 'spring-audi' did not exist, and `ask notacommand /tmp` complained
342
+ 'No such command /tmp' (the path, not the typo). Name the token the user
343
+ actually got wrong, and suggest the closest real commands.
344
+ """
345
+ matches = difflib.get_close_matches(token, sorted(_SUBCOMMANDS), n=3, cutoff=0.6)
346
+ first = f"error: '{token}' is not a known command"
347
+ if matches:
348
+ first += f". Did you mean: {', '.join(matches)}?"
349
+ else:
350
+ first += " and is not an existing directory to scan."
351
+ print(
352
+ first + "\n run 'ask --help' to list commands, or pass an existing "
353
+ "repository path.",
354
+ file=sys.stderr,
355
+ )
356
+ raise SystemExit(2)
357
+
358
+
359
+ def _has_trailing_nonsubcommand_positional(args: list[str], after: int) -> bool:
360
+ """True when a bare positional that is NOT a subcommand appears after index
361
+ ``after`` (options and the values they consume are skipped).
362
+
363
+ The implicit scan (`ask <path>`) owns exactly one positional, and a valid
364
+ invocation with two positionals always starts with a subcommand. So a first
365
+ non-subcommand positional followed by another non-subcommand positional —
366
+ `ask notacommand /tmp` — is malformed: the user meant a command. A trailing
367
+ token that IS a subcommand is left for ``_reject_path_before_subcommand``."""
368
+ skip_next = False
369
+ for arg in args[after + 1:]:
370
+ if skip_next:
371
+ skip_next = False
372
+ continue
373
+ if arg.startswith("-"):
374
+ if arg.split("=")[0] in _OPTIONS_WITH_VALUE and "=" not in arg:
375
+ skip_next = True
376
+ continue
377
+ if arg in _SUBCOMMANDS:
378
+ return False # wrong-order path/subcommand — handled elsewhere
379
+ return True
380
+ return False
381
+
382
+
334
383
  def _preprocess_args(args: list[str]) -> list[str]:
335
384
  """Extract a repository path token from an args list and store it in _detected_path.
336
385
 
@@ -363,7 +412,13 @@ def _preprocess_args(args: list[str]) -> list[str]:
363
412
  return result # known subcommand — leave for Click to dispatch
364
413
  if _path_index >= 0:
365
414
  continue # a later positional is the subcommand's own business
366
- # First genuine positional: treat as repository path
415
+ # First genuine positional. `ask` treats it as the repository path to scan
416
+ # (existence is validated downstream, not here). But if it is followed by
417
+ # another non-subcommand positional, the one-path scan cannot own both —
418
+ # `ask notacommand /tmp` — so the user meant a command: name the token they
419
+ # got wrong instead of swallowing it and erroring on the next one.
420
+ if _has_trailing_nonsubcommand_positional(result, i):
421
+ _reject_unknown_command(arg)
367
422
  _set_detected_path(arg)
368
423
  _path_index = i
369
424
  if _path_index >= 0:
@@ -1278,11 +1333,23 @@ def main(
1278
1333
  _raw_path_input = _get_detected_path()
1279
1334
  target = Path(_raw_path_input).resolve()
1280
1335
  if not target.exists():
1336
+ # A non-existent path that closely matches a command name is almost always a
1337
+ # mistyped subcommand (`ask spring-audi`) swallowed as an implicit-scan path,
1338
+ # not a real directory. Point at the likely command instead of only the path.
1339
+ _cmd_matches = difflib.get_close_matches(
1340
+ _raw_path_input, sorted(_SUBCOMMANDS), n=3, cutoff=0.6
1341
+ )
1342
+ _hint = "Pass an existing repository directory."
1343
+ if _cmd_matches:
1344
+ _hint = (
1345
+ f"If you meant a command, try: {', '.join('ask ' + m for m in _cmd_matches)}. "
1346
+ "Otherwise pass an existing repository directory."
1347
+ )
1281
1348
  _emit_error_json(
1282
1349
  INVALID_INPUT_CODE,
1283
1350
  f"Directory '{_raw_path_input}' does not exist.",
1284
1351
  path=_raw_path_input,
1285
- hint="Pass an existing repository directory.",
1352
+ hint=_hint,
1286
1353
  expected="An existing directory path.",
1287
1354
  )
1288
1355
  raise typer.Exit(code=1)
@@ -1784,6 +1851,10 @@ def main(
1784
1851
  )
1785
1852
  except Exception:
1786
1853
  pass # stale value better than crash
1854
+ # C1: token economy on the agent-facing views (cache-hit path).
1855
+ if format == "json" and (compact or agent):
1856
+ from sourcecode.token_estimate import inject_token_economy as _inj_te
1857
+ _cache_hit_content = _inj_te(_cache_hit_content, target)
1787
1858
  _emit_command_output(_cache_hit_content, output, copy)
1788
1859
  return
1789
1860
 
@@ -2632,7 +2703,14 @@ def main(
2632
2703
  }, indent=2, ensure_ascii=False)
2633
2704
  except Exception:
2634
2705
  pass
2635
- _emit_command_output(content, output, copy if not _pipeline_error else False)
2706
+ # C1: token economy on the agent-facing views (fresh path). Emit-only —
2707
+ # `content` stays clean so the cached L2 view never embeds a stale block
2708
+ # (the cache-hit path recomputes it against the working tree).
2709
+ _emit_content = content
2710
+ if format == "json" and (compact or agent):
2711
+ from sourcecode.token_estimate import inject_token_economy as _inj_te
2712
+ _emit_content = _inj_te(content, target)
2713
+ _emit_command_output(_emit_content, output, copy if not _pipeline_error else False)
2636
2714
 
2637
2715
  # Persist to two-layer cache (git SHA unchanged → re-use on next run).
2638
2716
  #
@@ -5528,7 +5606,7 @@ def spring_audit_cmd(
5528
5606
  help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
5529
5607
  ),
5530
5608
  ) -> None:
5531
- """Spring semantic audit: TX anomalies (TX-001..005) + security surface (SEC-001..003).
5609
+ """Spring semantic audit: TX anomalies (TX-001..006) + security surface (SEC-001..003).
5532
5610
 
5533
5611
  \b
5534
5612
  Detects:
@@ -5537,6 +5615,7 @@ def spring_audit_cmd(
5537
5615
  TX-003 readOnly=true boundary propagating to write operation
5538
5616
  TX-004 NOT_SUPPORTED/NEVER within active TX chain
5539
5617
  TX-005 Exception swallowing inside @Transactional
5618
+ TX-006 Self-invocation of @Transactional sibling (proxy bypass)
5540
5619
  SEC-001 Unsecured endpoint in annotation_based security model
5541
5620
  SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
5542
5621
  SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
@@ -5805,6 +5884,23 @@ def migrate_check_cmd(
5805
5884
  False, "--no-cache",
5806
5885
  help="Accepted for compatibility; this command always reads fresh source (no snapshot cache). No-op.",
5807
5886
  ),
5887
+ snapshot: bool = typer.Option(
5888
+ False, "--snapshot",
5889
+ help="Persist this readiness result as a timeline snapshot under --history-dir.",
5890
+ ),
5891
+ trend: bool = typer.Option(
5892
+ False, "--trend",
5893
+ help="Report the readiness trend across stored snapshots (days-remaining over time) "
5894
+ "instead of scanning. Reads --history-dir.",
5895
+ ),
5896
+ history_dir: Optional[Path] = typer.Option(
5897
+ None, "--history-dir",
5898
+ help="Directory of readiness snapshots (default: <repo>/.ask/readiness-history).",
5899
+ ),
5900
+ ref: Optional[str] = typer.Option(
5901
+ None, "--ref",
5902
+ help="Label for a --snapshot capture (e.g. a version or sprint tag).",
5903
+ ),
5808
5904
  ) -> None:
5809
5905
  """Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
5810
5906
 
@@ -5836,6 +5932,16 @@ def migrate_check_cmd(
5836
5932
  ask migrate-check /path/to/repo --format text
5837
5933
  ask migrate-check . --min-severity high
5838
5934
  ask migrate-check . --output migration.json
5935
+ ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
5936
+ ask migrate-check . --trend days-remaining over time
5937
+
5938
+ \b
5939
+ Readiness time series:
5940
+ --snapshot persists this run's budgetable figures (readiness_score, effort
5941
+ days, per-dimension scores, blocking_count) under --history-dir (default
5942
+ <repo>/.ask/readiness-history). --trend reads that series and reports
5943
+ first→last movement — no improving/degrading label, and readiness_score is
5944
+ flagged not-comparable when the applicable dimension set changed.
5839
5945
  """
5840
5946
  from sourcecode.repository_ir import find_java_files
5841
5947
  from sourcecode.migrate_check import run_migrate_check
@@ -5862,6 +5968,29 @@ def migrate_check_cmd(
5862
5968
  )
5863
5969
  raise typer.Exit(code=1)
5864
5970
 
5971
+ _history_dir = history_dir.resolve() if history_dir else (target / ".ask" / "readiness-history")
5972
+
5973
+ # --trend reads the stored series and reports movement over time; it does not scan.
5974
+ if trend:
5975
+ from sourcecode.readiness_timeline import build_readiness_trend, load_snapshots_dir
5976
+
5977
+ _snaps = load_snapshots_dir(_history_dir) if _history_dir.exists() else []
5978
+ if not _snaps:
5979
+ _emit_error_json(
5980
+ INVALID_INPUT_CODE,
5981
+ f"No readiness snapshots found in '{_history_dir}'.",
5982
+ path=str(_history_dir),
5983
+ hint="Capture points first: ask migrate-check <repo> --snapshot",
5984
+ expected="A directory holding readiness-snapshot-v1 artifacts.",
5985
+ )
5986
+ raise typer.Exit(code=1)
5987
+ _trend = build_readiness_trend(_snaps)
5988
+ _emit_command_output(
5989
+ _serialize_dict(_trend, "json"), output_path, copy,
5990
+ success_msg=f"Readiness trend over {_trend['count']} snapshot(s) → {_history_dir}",
5991
+ )
5992
+ return
5993
+
5865
5994
  _file_limitations: list[str] = []
5866
5995
  file_list = find_java_files(target, limitations=_file_limitations)
5867
5996
  _prog = Progress()
@@ -5882,6 +6011,14 @@ def migrate_check_cmd(
5882
6011
  payload = report.to_compact_dict() if compact else report.to_dict()
5883
6012
  output = _serialize_dict(payload, "json")
5884
6013
 
6014
+ _snapshot_note = ""
6015
+ if snapshot:
6016
+ from sourcecode.readiness_timeline import build_snapshot, write_snapshot
6017
+
6018
+ _snap = build_snapshot(report.to_dict(), ref=ref)
6019
+ _snap_path = write_snapshot(_snap, _history_dir)
6020
+ _snapshot_note = f"; snapshot → {_snap_path}"
6021
+
5885
6022
  _total = report.summary.get("total_findings", 0)
5886
6023
  _emit_command_output(
5887
6024
  output, output_path, copy,
@@ -5889,6 +6026,7 @@ def migrate_check_cmd(
5889
6026
  f"Migration check written to {output_path} "
5890
6027
  f"(score: {report.readiness_score if report.readiness_score is not None else 'N/A'}"
5891
6028
  f"{'/100' if report.readiness_score is not None else ''}, {_total} findings)"
6029
+ f"{_snapshot_note}"
5892
6030
  ),
5893
6031
  )
5894
6032
 
@@ -7897,11 +8035,15 @@ def cold_start_cmd(
7897
8035
  result["endpoints"] = result["endpoints"][:30]
7898
8036
  result["_meta"] = {**(result.get("_meta") or {}), "compact_mode": True,
7899
8037
  "full_available": "ask cold-start (without --compact)"}
8038
+ from sourcecode.token_estimate import estimate_tokens as _est_tokens, token_economy as _tok_econ
7900
8039
  _out = _json.dumps(result, indent=2, ensure_ascii=False)
7901
8040
  _size = len(_out.encode("utf-8"))
7902
- _tokens = _size // 4
8041
+ _tokens = _est_tokens(_out)
7903
8042
  _out_with_meta = _json.loads(_out)
7904
- _out_with_meta.setdefault("_meta", {})["estimated_tokens"] = _tokens
8043
+ _meta_cs = _out_with_meta.setdefault("_meta", {})
8044
+ _meta_cs["estimated_tokens"] = _tokens
8045
+ # C1: what this response costs vs. what reading the same files raw would.
8046
+ _meta_cs["token_economy"] = _tok_econ(_out, _out_with_meta, target)
7905
8047
  _out = _json.dumps(_out_with_meta, indent=2, ensure_ascii=False)
7906
8048
  if not compact and _size > 400_000:
7907
8049
  sys.stderr.write(
@@ -8018,7 +8160,9 @@ def mcp_init(
8018
8160
  typer.echo("No MCP clients found on this system.")
8019
8161
  typer.echo("")
8020
8162
  typer.echo("Manual setup — add to your MCP client config:")
8021
- typer.echo(' "sourcecode": {"command": "sourcecode", "args": ["mcp", "serve"]}')
8163
+ typer.echo(' "ask": {"command": "ask", "args": ["mcp", "serve"]}')
8164
+ typer.echo(' (VS Code keys these under "servers" and wants "type": "stdio";')
8165
+ typer.echo(' other clients use "mcpServers".)')
8022
8166
  raise typer.Exit(code=0)
8023
8167
 
8024
8168
  # Show detection results
@@ -8070,7 +8214,9 @@ def mcp_init(
8070
8214
  if a.client.config_path.exists():
8071
8215
  bak = backup.create(a.client.config_path)
8072
8216
  typer.echo(f" ✓ Backup {bak}")
8073
- updated = applier.apply_entry(config)
8217
+ updated = applier.apply_entry(
8218
+ config, a.client.servers_key, a.client.entry_extra
8219
+ )
8074
8220
  applier.write_config(a.client.config_path, updated)
8075
8221
  if not applier.validate(a.client.config_path):
8076
8222
  errors.append(f"{a.client.name}: JSON validation failed after write")
@@ -8154,16 +8300,18 @@ def mcp_status() -> None:
8154
8300
  typer.echo(f" Fix: ask mcp init --target {client.slug}")
8155
8301
  continue
8156
8302
  config = applier.read_config(client.config_path)
8157
- if applier.is_installed(config):
8303
+ if applier.is_installed(config, client.servers_key):
8158
8304
  typer.echo(f" {client.name:<20} ✓ configured {client.config_path}")
8159
8305
  # FIX-P0-5: inspect registered command for external-server drift.
8160
- _registered = config.get("mcpServers", {}).get("sourcecode", {})
8306
+ # Reads through the client's own servers key and entry name, so drift is
8307
+ # still detected for clients that key their servers differently.
8308
+ _registered = applier.registered_entry(config, client.servers_key)
8161
8309
  _reg_cmd = _registered.get("command", "")
8162
8310
  _reg_args = _registered.get("args", [])
8163
8311
  # Built-in form: command=sourcecode args=[mcp, serve] (or just the binary)
8164
8312
  _is_builtin = (
8165
- _reg_cmd == "sourcecode"
8166
- or (not _reg_args and _reg_cmd.endswith("/sourcecode"))
8313
+ _reg_cmd in ("ask", "sourcecode")
8314
+ or (not _reg_args and _reg_cmd.endswith(("/ask", "/sourcecode")))
8167
8315
  or (_reg_args and _reg_args[:2] == ["mcp", "serve"])
8168
8316
  )
8169
8317
  if _is_builtin:
@@ -8224,7 +8372,7 @@ def mcp_status() -> None:
8224
8372
  for _c in clients:
8225
8373
  if _c.app_installed:
8226
8374
  _cfg = applier.read_config(_c.config_path)
8227
- if applier.is_installed(_cfg):
8375
+ if applier.is_installed(_cfg, _c.servers_key):
8228
8376
  _configured_clients.add(_c.slug)
8229
8377
 
8230
8378
  # Stage 3: Process liveness — is the client app currently running?
@@ -8304,7 +8452,7 @@ def mcp_remove(
8304
8452
  bak = backup.create(a.client.config_path)
8305
8453
  typer.echo(f" ✓ Backup {bak}")
8306
8454
  config = applier.read_config(a.client.config_path)
8307
- updated = applier.remove_entry(config)
8455
+ updated = applier.remove_entry(config, a.client.servers_key)
8308
8456
  applier.write_config(a.client.config_path, updated)
8309
8457
  if not applier.validate(a.client.config_path):
8310
8458
  errors.append(f"{a.client.name}: JSON validation failed — restoring backup")
@@ -300,14 +300,18 @@ class FileClassifier:
300
300
  found = frozenset(m.group(1) for m in _JAVA_ANNOTATION_RE.finditer(content))
301
301
  if not found:
302
302
  return None
303
+ # Sorted, not set order: this list is emitted as `evidence` in the agent view,
304
+ # and frozenset iteration order varies with PYTHONHASHSEED — the same repo
305
+ # analysed twice produced the same evidence in a different order.
306
+ evidence = sorted(found)
303
307
  for required_annotations, category, relevance, why in _JAVA_STEREOTYPE_RULES:
304
308
  # For @Data DTO: must have @Data but NOT @Entity
305
309
  if required_annotations == frozenset({"Data"}):
306
310
  if "Data" in found and "Entity" not in found:
307
- return FileClassification(path, category, "high", relevance, why, list(found))
311
+ return FileClassification(path, category, "high", relevance, why, evidence)
308
312
  continue
309
313
  # For compound rules (Service+Transactional, Controller+RequestMapping): all required
310
314
  if required_annotations <= found:
311
- return FileClassification(path, category, "high", relevance, why, list(found))
315
+ return FileClassification(path, category, "high", relevance, why, evidence)
312
316
  return None
313
317
 
@@ -25,36 +25,50 @@ def read_config(path: Path) -> dict:
25
25
  return {}
26
26
 
27
27
 
28
- def is_installed(config: dict) -> bool:
28
+ def is_installed(config: dict, servers_key: str = _MCP_SERVERS_KEY) -> bool:
29
29
  """True if the ASK Engine entry (canonical `ask` or legacy `sourcecode`) is
30
- already present in mcpServers."""
31
- servers = config.get(_MCP_SERVERS_KEY, {})
30
+ already present in the client's servers map."""
31
+ servers = config.get(servers_key, {})
32
32
  return _ENTRY_NAME in servers or _LEGACY_ENTRY_NAME in servers
33
33
 
34
34
 
35
- def apply_entry(config: dict) -> dict:
36
- """Return new config dict with the canonical `ask` entry merged into mcpServers.
37
- Any legacy `sourcecode` entry is migrated away (removed) so the client launches
38
- a single server."""
35
+ def registered_entry(config: dict, servers_key: str = _MCP_SERVERS_KEY) -> dict:
36
+ """The ASK Engine server entry as currently registered, or an empty dict."""
37
+ servers = config.get(servers_key, {})
38
+ if not isinstance(servers, dict):
39
+ return {}
40
+ entry = servers.get(_ENTRY_NAME) or servers.get(_LEGACY_ENTRY_NAME) or {}
41
+ return entry if isinstance(entry, dict) else {}
42
+
43
+
44
+ def apply_entry(
45
+ config: dict,
46
+ servers_key: str = _MCP_SERVERS_KEY,
47
+ entry_extra: tuple[tuple[str, str], ...] = (),
48
+ ) -> dict:
49
+ """Return new config dict with the canonical `ask` entry merged into the client's
50
+ servers map. Any legacy `sourcecode` entry is migrated away (removed) so the client
51
+ launches a single server. *entry_extra* carries per-client fields (e.g. VS Code's
52
+ explicit `type: stdio`)."""
39
53
  config = dict(config)
40
- servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
54
+ servers: dict = dict(config.get(servers_key, {}))
41
55
  servers.pop(_LEGACY_ENTRY_NAME, None)
42
- servers[_ENTRY_NAME] = _ENTRY_VALUE
43
- config[_MCP_SERVERS_KEY] = servers
56
+ servers[_ENTRY_NAME] = {**_ENTRY_VALUE, **dict(entry_extra)}
57
+ config[servers_key] = servers
44
58
  return config
45
59
 
46
60
 
47
- def remove_entry(config: dict) -> dict:
61
+ def remove_entry(config: dict, servers_key: str = _MCP_SERVERS_KEY) -> dict:
48
62
  """Return new config dict with the ASK Engine entry removed — both the canonical
49
63
  `ask` key and any legacy `sourcecode` key."""
50
64
  config = dict(config)
51
- servers: dict = dict(config.get(_MCP_SERVERS_KEY, {}))
65
+ servers: dict = dict(config.get(servers_key, {}))
52
66
  servers.pop(_ENTRY_NAME, None)
53
67
  servers.pop(_LEGACY_ENTRY_NAME, None)
54
68
  if servers:
55
- config[_MCP_SERVERS_KEY] = servers
56
- elif _MCP_SERVERS_KEY in config:
57
- del config[_MCP_SERVERS_KEY]
69
+ config[servers_key] = servers
70
+ elif servers_key in config:
71
+ del config[servers_key]
58
72
  return config
59
73
 
60
74