sourcecode 5.8.12__py3-none-any.whl → 5.8.14__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.
sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "5.8.12"
7
+ __version__ = "5.8.14"
@@ -16,23 +16,49 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
16
16
 
17
17
  ## Current Synchronization
18
18
 
19
- **As of 2026-08-19, the external `5.8.11` re-audit:** this is the current status of
20
- the 5.8.8/5.8.9 audit queue. The subject remained `saint-server` at `3dde0376`, with a
21
- byte-identical CIR and the same 53/61 invocables exercised. The score is **78/100**
22
- (75 in 5.8.8, 80 in 5.8.9). The observations below remain historical evidence; only
23
- this table declares present state.
19
+ **As of 2026-08-20, release `5.8.14` and the fourth external re-audit:** the audited
20
+ snapshot remains `3dde0376`; the report compares the four audit rounds and scores this one
21
+ **90/100** (75, 80, 78, 90). It exercised 53/61 invocables (87%), ran no application and
22
+ left the repository unchanged. Its 40+ longitudinal functional anchors are unchanged, so
23
+ there is no reported product regression. The `5.8.14` release ships the implementations
24
+ recorded below; they remain distinguished from facts verified on the `5.8.13` subject.
25
+ Only this table declares present state.
26
+
27
+ **Release status:** entries marked “pending `5.8.14`” in their historical wording are
28
+ shipped in `5.8.14`; a re-audit is still required wherever the row says so.
24
29
 
25
30
  | Rows | Current status | Evidence |
26
31
  |---|---|---|
27
- | `AUD-588-B01`, `AUD-589-B01`, `C3-122` | **implemented in 5.8.12** | `9884488` requires `risk` and `audit-report` to deliver stdout or their requested artifact before exit 0; the remaining closure evidence is a field-scale subprocess reproduction. |
32
+ | `AUD-588-B01` / `AUD-589-B01`, `C3-122` | **open, P0 for `risk`; `audit-report` corrected** | `9884488` corrected `audit-report` to refuse with typed `OUTPUT_TOO_LARGE` and produce its requested artifact. `risk` nevertheless exited 0 twice on the first shared-cache invocations with 0 bytes and no `-o` file, then succeeded seven times. `5cfa9ba` now publishes the source, audit-finding, candidate-defect and composed-defect working set, including partial answers. Repeat the real entry-point protocol before closure. |
28
33
  | `AUD-511-R01` / `AUD-588-B03` | **implemented in 5.8.12** | `13362ec` makes SEC-009 read comment-blanked Java, so a Javadoc profile cannot override the live `@Profile("!m3")` declaration. |
29
34
  | `AUD-588-B04` | **retracted audit premise** | `data-exposure` correctly preserves `chain_decision`; the reported `coverage_unknown` routes were custom-gate annotations whose application was not configured. `fd34eb1` remains a valid additive disclosure, but no further B04 fix is due. |
30
35
  | `AUD-588-B05` | **closed** | `c15f5b6`; changed container-wired components block a false `unaffected` disposition. |
31
- | `AUD-511-R03` / `AUD-588-B07` | **implemented in 5.8.12** | `47aee1e` gives the published `OUTPUT_TOO_LARGE` envelope precedence over an inconsistent process status and preserves the excerpt in every branch. |
36
+ | `AUD-511-R03` / `AUD-588-B07` | **implemented, pending 5.8.14 re-audit** | `f613df9` makes ASK-09 parse the typed document from stdout or stderr, including a progress-prefixed stderr stream, and removes the four inherited variables that change its precondition. The regression exercises the real typed refusal shape. The prior claim that the payload premise was false remains retracted as an environment-contamination artifact. |
32
37
  | `AUD-588-B08` | **closed** | `c084555`; `impact` publishes its independent stdout-byte budget. |
33
- | `AUD-588-B11` (container/units subset) | **partially implemented in 5.8.12** | `d745d31` discovers nested Maven modules; `273ae8e` suppresses unsupported `pr-impact.unaffected_basis`. `impact` naming, source population units, risk tiers and plan narrative remain open. |
34
- | `AUD-511-R02` / `AUD-589-B02` | **partially mitigated in 5.8.12, still under investigation** | `b93f230` reuses one `SpringSemanticModel` across every declared `data-exposure` seed; `473f47a` routes `validation` through the shared CIR cache, and `91bd0c0` retains subcommand `--no-cache` compatibility. On the local BroadleafCommerce corpus at the final analyzer fingerprint, a first shared-CIR fill took 10.46 s and its warm `validation --compact` hit took 4.01 s with byte-identical output. This is a same-host cache-state observation, not the 5.8.9/5.8.11 external A/B: the six-run C3-120 comparison and the configured `saint-server` reproduction remain required. |
35
- | `AUD-588-B02`, `B06`, B11 residuals, `B12` | **partially open in 5.8.12** | `3869c87` excludes explicit EclipseLink from Hibernate applicability and effort; `037223e` publishes provider non-coverage. B11 parity and controlled cost calibration remain required. |
38
+ | `MCP-001` | **closed 5.8.13** | Deep corpus testing reproduced concurrent `CliRunner` stream capture leaking a tool response to the host stdout. `b2c9660` serializes the narrow process-global capture seam; concurrent real MCP calls and a regression test verify response isolation. |
39
+ | `AUD-513-N01` (`N-1`) | **closed in 5.8.14** | `rename-class` rewrote three Petclinic files under both `ASK_READONLY=1` and `--no-write`, then exited 0. The audit restored the tree; this is a write-policy breach, not an artifact-directory exception. `readonly.guard_mutation()` now refuses before any planned source write or physical rename; help names the command as mutating and regressions cover both readonly controls plus `--dry-run`. |
40
+ | `AUD-513-N02` / `B-4` / `N-5` | **N02/B-4 implemented in 5.8.14; N-5 open** | `posture.summary.unconditional` counted 1,697 on a non-Spring Struts tree and 1,573 on Mall, and `security` was `0/0/0` with no population. The posture projection now uses the canonical Spring IoC bean population, publishes its population/unit and separates unconditional security beans. Regressions cover non-Spring, mixed-bean and conditional/unconditional security fixtures. Progress/advice still use incompatible “Java files” scopes (`N-5`), so the `AUD-588-F02` family remains open. |
41
+ | `AUD-513-B01` / `B-3` / `B-5` | **B01/B-5 implemented in 5.8.14; B-3 closed as revalidated** | A MyBatis XML paired with a `*Mapper.java` interface now takes precedence over an absent `@Mapper` annotation, preserving XML-backed/`@MapperScan` interfaces in `mapper_interfaces` rather than misclassifying them as DTO mappers. `imports_found` now contains only actual Java imports; code, XML and build matches are emitted as `evidence`, including in compact output. `B-3` was stale against the current source: both views consume `JAVA_SPRING_SECTIONS`; a real MyBatis Mapper/XML regression proves the section is retained by `--agent`. |
42
+ | `AUD-513-N03/N04/N07/N08/N09`, `B-6/B-7/B-8`, `D-09` | **N03/N04/N07/N08/N09 implemented in 5.8.14; remainder open** | `impact` now always publishes the `candidates` field its not-found message names, including `[]` when no close symbol exists. Budget advice no longer calls partial snapshot/RIS evidence a warm cache: it identifies that presence and says context/parse are unknown before analysis, whose payload then reports all four actual layers. README operational sections (`clone`, install, build, getting started, troubleshooting and related headings) are never promoted into a project summary; with no descriptive prose compact output publishes `project_summary: null` and `summary_basis`. `security_posture.limitations` is now cache-state invariant; dynamic execution advice travels in `security_posture.operational_hints`. Every numeric output bound rejects negative values at parse time with a structured `INVALID_INPUT` envelope (`flag`, `value`, `expected`); `--limit 0` still means no cap wherever that behavior was published. Budgets overrun, timing coverage is inconsistent, compact hides unresolved identities and cost anchors remain stale. |
43
+ | `AUD-588-B11` (container/units subset) | **partially implemented in 5.8.14** | `d745d31` discovers nested Maven modules; `273ae8e` suppresses unsupported `pr-impact.unaffected_basis`. `e53507d` separates direct impact matches from analysis seeds and implementation classes; `8386744` carries the established container-wiring fact into plan checklists and compare cost dimensions. Source population units and risk-tier wording remain open. |
44
+ | `AUD-511-R02` / `AUD-589-B02` / `AUD-513-N06` | **partially corrected; `data-exposure` open, P1** | Nine of ten measured commands recovered, many to their best historical timings. `data-exposure --config` did not: 42.7 s versus 16.2 s in 5.8.8 and a 28.9 s model, with byte-equivalent output. Measure it with the idle-host, isolated-cache, interleaved-control protocol before changing code, then compare its reuse path with `validation`. |
45
+ | `AUD-588-B02`, `B06`, B11 residuals, `B12` | **B02/B06 closed; B11/B12 open, P2** | `3869c87` excludes explicit EclipseLink from Hibernate applicability and effort; `037223e` publishes provider non-coverage. The re-audit verifies both and the payload reduction; `cf57457` makes aggregate wording name only applicable dimensions. Complete B11 consumer parity and calibrate B12 only from provenance-bearing controlled samples. |
46
+
47
+ ### Fourth Re-audit: 5.8.13
48
+
49
+ The fourth pass verifies the current facts that supersede the prior intake. There is no
50
+ crash/traceback and no regression in the published anchors. `B-2/D-05` remains a retracted
51
+ servlet premise, and the audit additionally verifies EclipseLink applicability/non-coverage,
52
+ typed `audit-report` refusal, nested Maven discovery and the `unaffected_basis` correction.
53
+ It leaves the following concrete queue rather than reopening closed facts.
54
+
55
+ | Recommendation | Status and direction |
56
+ |---|---|
57
+ | `AUD-588-F02` shared answer contracts | **Partial.** Reuse the existing authorities for exact target versus implementation cone, named source populations and container reach in plan/compare; do not add a parallel detector. |
58
+ | `AUD-588-F03` versioned cost calibration | **Open.** Capture repository, cache state, sample count and host provenance. The immediately actionable measurement is `data-exposure --config`; clean data is required before recalibration. |
59
+ | Risk cache observability | **Recommended hardening.** Publish working-set unit counts under progress or in the envelope to distinguish an empty compositor from a present-but-incomplete cache. |
60
+ | `AUD-588-F05` coverage expansion | **Locally verified only.** Dirty-tree `verify-edit` and the eight mutating/denied invocables remain unexercised in all four external rounds. |
61
+ | EclipseLink migration automation | **Deferred product feature.** The current applicability and non-coverage facts are verified; provider-specific migration automation is not a corrective fix. |
36
62
 
37
63
  ### Re-audit Intake: 5.8.11
38
64
 
@@ -594,7 +620,7 @@ class's subject, not its provenance.
594
620
 
595
621
  | ID | Item | Found | Severity | Status |
596
622
  |---|---|---|---|---|
597
- | P-1 | Telemetry is **opt-out**. Auditing a public-sector health client's code, it must be disabled *before* the first run, not after | 3.2.0 | **High (procurement)** | **closed 3.3.0** — opt-in: with no explicit choice on record, `is_enabled()` is False everywhere, so a fresh install transmits nothing on the run that prints the notice. The CI special case is gone with the default it existed to override; CI is still detected for one purpose, skipping a notice that has no terminal to appear on. The first-run notice became an invitation that states what *would* be collected. `telemetry status` distinguishes "no choice recorded" from "you chose disabled" (`stored_choice()` returns `None`, not `False`) — a buyer auditing this needs to know which one they have. An explicit choice made before the change is honoured unchanged |
623
+ | P-1 | Telemetry default and procurement disclosure | 3.2.0 | **High (procurement)** | **reopened and corrected 5.8.12** — telemetry is on by default by explicit product decision, with `ask telemetry disable`, `SOURCECODE_TELEMETRY=0` and `DO_NOT_TRACK=1` honored before any event is sent. The first-run notice discloses the fields and the disable path. `telemetry status` distinguishes the default from an explicit disabled choice; the privacy policy documents the endpoint, 90-day retention and bounded anonymous fields. This supersedes the historical 3.3.0 opt-in closure for the current release |
598
624
  | P-2 | Licence state is ambiguous: `auth status` → `{"status":"unauthenticated","pro":true,"pro_reason":"early-adoption unlock"}`. The buyer cannot tell what it will cost or what they lose when the door closes | 3.2.0 | Medium | **closed 3.4.0** — one authority, `license.entitlement()`: `entitlement` (what runs), `source` (why: `license_key` / `early_adoption_unlock` / `free_tier`), `authenticated` (a separate fact — today one can be entitled without a credential), `paywall_active`, and `when_it_changes`, which states what the user loses when that source stops applying, in the terms the gate uses and **never as a price**. `is_pro` — what every gate reads — is now derived from it rather than computed beside it, which is how the status page came to contradict the commands. The legacy keys stay for consumers, are derived from the authority, and are listed in `deprecated_fields` |
599
625
 
600
626
  | P-3 | **The price is not published.** With P-2 closed the entitlement is unambiguous, but a buyer still cannot see what Pro costs or what a team of 25 pays. Eval #5 lists *"precio Pro público"* among the conditions for the top of its price band, beside the robustness fixes | 3.2.1 (eval #5) | Medium (procurement) | open — packaging question, not a page: the field's own reading is that **the value is frontal** (a first audit replaces 5–15 person-days) **and the recurring half is the CI gate**, so a full seat subscription on a product whose value peaks on run one *"genera churn en el mes 4"*. Model to test: one-off assessment **3–5 k€ per repo** + **8–12 €/dev/month** for the gate. Depends on C3-22: without the gate there is nothing recurring to sell |
@@ -138,7 +138,7 @@ pipx install sourcecode # isolated install, no venv needed
138
138
 
139
139
  # Verify
140
140
  ask version
141
- # ask 5.8.12
141
+ # ask 5.8.14
142
142
  ```
143
143
 
144
144
  Requires Python 3.9+.
@@ -1199,7 +1199,7 @@ explaining. The rest do one thing; `ask <command> --help` is the whole story.
1199
1199
  | `ask retrieve` | parked | Typed knowledge queries over the model — kept working, no longer developed |
1200
1200
  | `ask baseline capture\|diff\|trend` | supported | Versioned architectural metrics over time; see [`ask trend`](#ask-trend-dir--how-the-architecture-moved) |
1201
1201
  | `ask cache status\|warm\|model\|clear\|freshness` | supported | Cache inspection; `ask cache model` states what a warm buys each command |
1202
- | `ask config` · `ask version` · `ask activate` · `ask auth` · `ask telemetry` · `ask mcp` | supported | Configuration, version, licence, authentication, telemetry (off by default), MCP integration |
1202
+ | `ask config` · `ask version` · `ask activate` · `ask auth` · `ask telemetry` · `ask mcp` | supported | Configuration, version, licence, authentication, telemetry (on by default), MCP integration |
1203
1203
 
1204
1204
  ## Typical workflows
1205
1205
 
@@ -1287,7 +1287,7 @@ ask mcp serve
1287
1287
  ask mcp remove
1288
1288
  ```
1289
1289
 
1290
- The MCP server exposes structural analysis tools to AI agents without requiring the agent to call the CLI directly. Claude Desktop and Cursor can query impact, endpoints, and context through the MCP protocol.
1290
+ The MCP server exposes structural analysis tools to AI agents without requiring the agent to call the CLI directly. Claude Desktop and Cursor can query impact, endpoints, and context through the MCP protocol. Concurrent requests are accepted safely: ASK serializes only the in-process CLI capture seam, because its stdout/stderr redirection is process-global, so one tool response cannot contaminate another.
1291
1291
 
1292
1292
  ---
1293
1293
 
sourcecode/change_plan.py CHANGED
@@ -139,7 +139,9 @@ def build_change_plan(
139
139
  ),
140
140
  }
141
141
 
142
- seeds = list(blast["matched_fqns"])
142
+ # The user-facing match names the requested target; planning must retain the
143
+ # subtype-expanded seeds that the blast-radius authority actually analysed.
144
+ seeds = list(blast.get("analysis_seed_fqns") or blast["matched_fqns"])
143
145
  container_wired = [w.to_dict() for w in detect_classes(cir, seeds)]
144
146
  meta = _node_meta(cir)
145
147
  affected = _dependents_closure(cir.reverse_graph or {}, seeds, max_depth, _NODE_CAP)
@@ -210,6 +212,11 @@ def build_change_plan(
210
212
  checklist.append(
211
213
  f"{len(sec)} secured endpoint(s) in scope — re-check authorization."
212
214
  )
215
+ if container_wired:
216
+ checklist.append(
217
+ f"{len(container_wired)} container-wired component(s) are in scope — "
218
+ "review their declared framework wiring because fan-in does not measure it."
219
+ )
213
220
  txn = blast.get("transactional_boundaries_touched") or []
214
221
  if txn:
215
222
  checklist.append(f"{len(txn)} transactional boundary/boundaries in scope.")
sourcecode/cli.py CHANGED
@@ -6,6 +6,7 @@ import hashlib
6
6
  import json
7
7
  import difflib
8
8
  import os
9
+ import re
9
10
  import sys
10
11
  import tempfile
11
12
  import threading
@@ -287,6 +288,7 @@ COMMANDS_THAT_WRITE: "tuple[tuple[str, str], ...]" = (
287
288
  ("migrate-check", "`<repo>/.ask/readiness-history/` — only with `--history`"),
288
289
  ("verify", "`<repo>/.ask/contracts-baseline.json` — only with `--update-baseline`"),
289
290
  ("verify-edit", "the repository's git hooks directory — only with `--install-hook`"),
291
+ ("rename-class", "`<repo>/**/*.java` — only without `--dry-run`; existing sources change and the declaring file is renamed"),
290
292
  ("*", "`<repo>/.ask/runs/` — only when `ASK_RUNS_IN_REPO=1` is set"),
291
293
  )
292
294
 
@@ -294,7 +296,7 @@ COMMANDS_THAT_WRITE: "tuple[tuple[str, str], ...]" = (
294
296
  def _writes_help_block() -> str:
295
297
  """The `--no-write` table as `--help` prints it — generated, never typed."""
296
298
  lines = [
297
- "Commands that create files inside the repository (everything else reads only):",
299
+ "Commands that modify files inside the repository (everything else reads only):",
298
300
  "",
299
301
  ]
300
302
  width = max(len(name) for name, _ in COMMANDS_THAT_WRITE)
@@ -1625,7 +1627,10 @@ def _budget_evidence(command: str, path: "Optional[Path]") -> "tuple[Optional[st
1625
1627
  if path is not None:
1626
1628
  measured = _plan.scope_for(Path(path))
1627
1629
  if measured is not None and measured.measured:
1628
- scope = measured.describe()
1630
+ # The scope probe can cheaply see only the snapshot/RIS store.
1631
+ # Do not turn that partial observation into "cache warm" while
1632
+ # a command's actual context/parse state is still unknown.
1633
+ scope = measured.describe_for_budget()
1629
1634
  except Exception:
1630
1635
  pass # a message about cost must never be the reason a run fails
1631
1636
  return anchors, scope
@@ -2325,6 +2330,32 @@ def _root_flag_usage(flag: str) -> str:
2325
2330
  return f"`ask {flag} <path>`"
2326
2331
 
2327
2332
 
2333
+ def _numeric_range_error_context(message: str) -> dict[str, object]:
2334
+ """Extract structured facts from Click's lower-bound validation message.
2335
+
2336
+ Click owns parser-time validation, so commands never start work for an
2337
+ invalid value. Its stock message contains the useful facts but leaves
2338
+ automation to parse prose; expose those facts in the shared error envelope.
2339
+ """
2340
+ prefix, separator, remainder = message.partition(": ")
2341
+ if not separator or not prefix.startswith("Invalid value for "):
2342
+ return {}
2343
+ minimum = re.search(
2344
+ r"is not in (?:the )?range (?:x>=(\d+)|(\d+)<=x(?:<=\d+)?)",
2345
+ remainder,
2346
+ )
2347
+ value = re.match(r"(-?\d+)\s+is not in", remainder)
2348
+ flags = re.findall(r"'(--[^']+)'", prefix)
2349
+ if minimum is None or value is None or not flags:
2350
+ return {}
2351
+ lower_bound = minimum.group(1) or minimum.group(2)
2352
+ return {
2353
+ "flag": flags[0],
2354
+ "value": int(value.group(1)),
2355
+ "expected": f"an integer >= {lower_bound}",
2356
+ }
2357
+
2358
+
2328
2359
  try:
2329
2360
  import click.exceptions as _click_exc
2330
2361
 
@@ -2386,7 +2417,11 @@ try:
2386
2417
 
2387
2418
  def _json_click_usage_error_show(self: Any, file: Any = None) -> None: # type: ignore[override]
2388
2419
  import json as _je
2420
+ _message = self.format_message()
2421
+ _numeric_context = _numeric_range_error_context(_message)
2389
2422
  _flag, _hint = _misplaced_global_flag_hint(self)
2423
+ if _numeric_context:
2424
+ _flag = str(_numeric_context["flag"])
2390
2425
  if not _hint:
2391
2426
  _nearby = _nearby_command_flag_hint(self)
2392
2427
  if _nearby:
@@ -2396,10 +2431,13 @@ try:
2396
2431
  _context["flag"] = _flag
2397
2432
  if _hint:
2398
2433
  _context["scope"] = "root"
2434
+ if _numeric_context:
2435
+ _context["value"] = _numeric_context["value"]
2399
2436
  payload = build_error_envelope(
2400
2437
  INVALID_INPUT_CODE,
2401
- self.format_message(),
2438
+ _message,
2402
2439
  hint=_hint,
2440
+ expected=(str(_numeric_context["expected"]) if _numeric_context else None),
2403
2441
  **_context,
2404
2442
  )
2405
2443
  sys.stderr.write(_je.dumps(payload, ensure_ascii=_json_ensure_ascii()) + "\n")
@@ -3033,13 +3071,59 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
3033
3071
 
3034
3072
  _orig_cmd_main = cmd.main
3035
3073
 
3074
+ _TELEMETRY_COMMANDS = frozenset({
3075
+ "repo-ir", "impact", "endpoints", "export", "validation", "delta", "contract-diff",
3076
+ "plan", "compare", "trend", "timeline", "spring-audit", "verify-edit", "verify", "risk",
3077
+ "enrich", "audit-report", "migrate-recipe", "data-exposure", "posture", "migrate-check",
3078
+ "impact-chain", "pr-impact", "explain", "onboard", "review-pr", "fix-bug", "modernize",
3079
+ "rename-class", "chunk-file", "activate", "selftest", "explain-endpoint", "regress", "version",
3080
+ "schema", "config", "archetype", "cold-start", "telemetry", "mcp", "cache", "baseline",
3081
+ "auth", "retrieve", "prepare-context",
3082
+ })
3083
+
3084
+ def _telemetry_command(args_for_command: Optional[list[str]]) -> str:
3085
+ """Extract only a registered CLI command, never a path or option value."""
3086
+ tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
3087
+ for token in tokens:
3088
+ if token in _TELEMETRY_COMMANDS:
3089
+ return token
3090
+ return "analyze"
3091
+
3092
+ def _telemetry_flags(args_for_command: Optional[list[str]]) -> list[str]:
3093
+ tokens = list(args_for_command) if args_for_command is not None else sys.argv[1:]
3094
+ try:
3095
+ from sourcecode.telemetry.filters import _SAFE_FLAGS
3096
+ return sorted({token for token in tokens if token in _SAFE_FLAGS})
3097
+ except Exception:
3098
+ return []
3099
+
3036
3100
  def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
3037
3101
  if args is not None:
3038
3102
  # CliRunner / programmatic call: preprocess the explicit args list.
3039
3103
  _set_detected_path(".")
3040
3104
  args = _preprocess_args(list(args))
3041
3105
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
3042
- return _orig_cmd_main(args=args, **kwargs)
3106
+ started = time.monotonic()
3107
+ command = _telemetry_command(args)
3108
+ success = True
3109
+ try:
3110
+ return _orig_cmd_main(args=args, **kwargs)
3111
+ except BaseException:
3112
+ success = False
3113
+ raise
3114
+ finally:
3115
+ try:
3116
+ from sourcecode import telemetry as _tel
3117
+ _tel.record(
3118
+ "execution_completed",
3119
+ cmd=("mcp" if command == "mcp" else "telemetry" if command == "telemetry" else "analyze"),
3120
+ command=command,
3121
+ flags=_telemetry_flags(args),
3122
+ duration_s=time.monotonic() - started,
3123
+ success=success,
3124
+ )
3125
+ except Exception:
3126
+ pass
3043
3127
 
3044
3128
  cmd.main = _cmd_main
3045
3129
  return cmd
@@ -3055,7 +3139,7 @@ try:
3055
3139
  except Exception:
3056
3140
  pass
3057
3141
 
3058
- telemetry_app = typer.Typer(help="Manage anonymous telemetry (off by default; opt-in).", rich_markup_mode="rich")
3142
+ telemetry_app = typer.Typer(help="Manage anonymous telemetry (on by default; disable any time).", rich_markup_mode="rich")
3059
3143
  app.add_typer(telemetry_app, name="telemetry")
3060
3144
 
3061
3145
  mcp_app = typer.Typer(help="MCP integration: setup, status, serve, remove.", rich_markup_mode="rich")
@@ -3083,9 +3167,8 @@ app.add_typer(retrieve_app, name="retrieve")
3083
3167
  def _maybe_show_telemetry_notice() -> None:
3084
3168
  """Show first-run telemetry notice once, on interactive TTYs only.
3085
3169
 
3086
- Telemetry is off by default (opt-in). The notice is an invitation, not a
3087
- disclosure: nothing has been collected when it appears. Marked as shown so it
3088
- appears only once.
3170
+ Telemetry is on by default. The notice is a one-time disclosure of what is
3171
+ collected and how to disable it.
3089
3172
  """
3090
3173
  try:
3091
3174
  from sourcecode.telemetry.config import has_been_asked, mark_asked
@@ -5041,7 +5124,9 @@ def main(
5041
5124
  sm.key_dependencies = _deduped_deps # no cap — all direct deps included
5042
5125
 
5043
5126
  # LQN-02: deterministic NL summary
5044
- sm.project_summary = ProjectSummarizer(target).generate(sm)
5127
+ _summarizer = ProjectSummarizer(target)
5128
+ sm.project_summary = _summarizer.generate(sm)
5129
+ sm.summary_basis = _summarizer.summary_basis
5045
5130
  sm.architecture_summary = ArchitectureSummarizer(target).generate(sm)
5046
5131
 
5047
5132
  # Phase 13 Plan 04: Architectural Inference (--architecture flag)
@@ -5314,26 +5399,7 @@ def main(
5314
5399
  perf.stop("serialize", _perf_serialize)
5315
5400
  perf.flush_recorder()
5316
5401
 
5317
- # 5. Telemetry (fire-and-forget, never blocks)
5318
- try:
5319
- from sourcecode import telemetry as _tel
5320
- _tel.record(
5321
- "execution_completed",
5322
- cmd="analyze",
5323
- flags=_active_flags(
5324
- dependencies, graph_modules, docs, full_metrics,
5325
- semantics, architecture, git_context, env_map,
5326
- code_notes, agent, compact, tree, no_redact, format,
5327
- ),
5328
- output_fmt=format,
5329
- file_count=len(sm.file_paths),
5330
- duration_s=time.monotonic() - _t0,
5331
- success=True,
5332
- )
5333
- except Exception:
5334
- pass
5335
-
5336
- # 6. Write output (CLI-04)
5402
+ # 5. Write output (CLI-04)
5337
5403
  _progress.finish()
5338
5404
  if format == "json":
5339
5405
  _uncommitted_fresh, _uncommitted_fresh_basis = _uncommitted_fact(
@@ -5926,17 +5992,6 @@ def prepare_context_cmd(
5926
5992
  except Exception:
5927
5993
  pass # not JSON (or unreadable) → serve exactly what was stored
5928
5994
  _emit_command_output(_cached_pctx, output_path, copy)
5929
- try:
5930
- from sourcecode import telemetry as _tel
5931
- _tel.record(
5932
- "execution_completed",
5933
- cmd="prepare-context",
5934
- feature=task,
5935
- output_fmt=format,
5936
- duration_s=0.0,
5937
- )
5938
- except Exception:
5939
- pass
5940
5995
  return
5941
5996
 
5942
5997
  _scope_files = None
@@ -6463,18 +6518,6 @@ def prepare_context_cmd(
6463
6518
  fmt=("yaml" if format == "yaml" else "json")
6464
6519
  )
6465
6520
 
6466
- try:
6467
- from sourcecode import telemetry as _tel
6468
- _tel.record(
6469
- "execution_completed",
6470
- cmd="prepare-context",
6471
- feature=task,
6472
- output_fmt=format,
6473
- duration_s=_time.perf_counter() - _t0,
6474
- )
6475
- except Exception:
6476
- pass
6477
-
6478
6521
  from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
6479
6522
  _nudge()
6480
6523
 
@@ -6491,11 +6534,11 @@ def telemetry_status(
6491
6534
  enabled = is_enabled()
6492
6535
  choice = stored_choice()
6493
6536
  status = "enabled" if enabled else "disabled"
6494
- lines = [f"Telemetry: {status} (off by default; opt-in)"]
6495
- # "off because you said so" and "off because nobody asked you" are different
6496
- # answers, and a buyer auditing this needs to be told which one they have.
6537
+ lines = [f"Telemetry: {status} (on by default; disable with `ask telemetry disable`)"]
6538
+ # The default and an explicit disabled choice are different answers, and a
6539
+ # buyer auditing this needs to be told which one they have.
6497
6540
  if choice is None:
6498
- lines.append(" No choice recorded — nothing has been collected or sent.")
6541
+ lines.append(" No choice recorded — the on-by-default setting applies.")
6499
6542
  else:
6500
6543
  lines.append(f" Your recorded choice: {'enabled' if choice else 'disabled'}.")
6501
6544
  lines.append(f" Config: {config_file_path()}")
@@ -6508,7 +6551,7 @@ def telemetry_status(
6508
6551
 
6509
6552
  @telemetry_app.command("enable")
6510
6553
  def telemetry_enable() -> None:
6511
- """Opt in to anonymous telemetry."""
6554
+ """Enable anonymous telemetry and remember the choice."""
6512
6555
  from sourcecode.telemetry.config import set_enabled
6513
6556
  from sourcecode import telemetry as _tel
6514
6557
  set_enabled(True)
@@ -6521,11 +6564,11 @@ def telemetry_enable() -> None:
6521
6564
 
6522
6565
  @telemetry_app.command("disable")
6523
6566
  def telemetry_disable() -> None:
6524
- """Opt out of anonymous telemetry."""
6567
+ """Disable anonymous telemetry and remember the choice."""
6525
6568
  from sourcecode.telemetry.config import set_enabled
6526
6569
  set_enabled(False)
6527
6570
  typer.echo("Telemetry disabled. No data will be collected or sent.")
6528
- typer.echo("Telemetry is off by default; this choice is recorded so the notice stops asking.")
6571
+ typer.echo("Telemetry is enabled by default; this choice is recorded so the notice stops appearing.")
6529
6572
  typer.echo("Re-enable at any time: ask telemetry enable")
6530
6573
 
6531
6574
 
@@ -6590,11 +6633,13 @@ def repo_ir_cmd(
6590
6633
  None,
6591
6634
  "--max-nodes",
6592
6635
  help="Limit graph.nodes to top N by impact score (reduces output size)",
6636
+ min=0,
6593
6637
  ),
6594
6638
  max_edges: Optional[int] = typer.Option(
6595
6639
  None,
6596
6640
  "--max-edges",
6597
6641
  help="Limit graph.edges to N (priority: edges between kept nodes)",
6642
+ min=0,
6598
6643
  ),
6599
6644
  summary_only: bool = typer.Option(
6600
6645
  False,
@@ -7185,6 +7230,7 @@ def endpoints_cmd(
7185
7230
  limit: Optional[int] = typer.Option(
7186
7231
  None, "--limit", "-n",
7187
7232
  help="Maximum number of endpoints to return.",
7233
+ min=0,
7188
7234
  ),
7189
7235
  compact: bool = typer.Option(
7190
7236
  False, "--compact",
@@ -7943,6 +7989,7 @@ def validation_cmd(
7943
7989
  None, "--limit",
7944
7990
  help="Keep at most N rows per list (endpoints, gaps, validators); the "
7945
7991
  "summary stays whole and each cut list declares what it omitted.",
7992
+ min=0,
7946
7993
  ),
7947
7994
  compact: bool = typer.Option(
7948
7995
  False, "--compact",
@@ -10627,7 +10674,7 @@ def risk_cmd(
10627
10674
  help="Repository path (default: current directory).",
10628
10675
  ),
10629
10676
  limit: int = typer.Option(
10630
- 50, "--limit", help="How many composed risks to publish (highest first)."
10677
+ 50, "--limit", help="How many composed risks to publish (highest first).", min=0,
10631
10678
  ),
10632
10679
  table: bool = typer.Option(
10633
10680
  False,
@@ -10838,7 +10885,7 @@ def enrich_cmd(
10838
10885
  ),
10839
10886
  ),
10840
10887
  limit: int = typer.Option(
10841
- 50, "--limit", help="How many enriched findings to publish (highest first)."
10888
+ 50, "--limit", help="How many enriched findings to publish (highest first).", min=0,
10842
10889
  ),
10843
10890
  table: bool = typer.Option(
10844
10891
  False,
@@ -11498,6 +11545,7 @@ def posture_cmd(
11498
11545
  "--limit",
11499
11546
  help="Keep at most N beans per list (active/inactive/unresolved); the "
11500
11547
  "counts stay whole and each cut list declares what it omitted.",
11548
+ min=0,
11501
11549
  ),
11502
11550
  compact: bool = typer.Option(
11503
11551
  False,
@@ -11985,6 +12033,8 @@ def migrate_check_cmd(
11985
12033
  depth: int = typer.Option(
11986
12034
  4, "--depth",
11987
12035
  help="Caller BFS depth for --blast-radius (1-8, default: 4).",
12036
+ min=1,
12037
+ max=8,
11988
12038
  ),
11989
12039
  detach: bool = _detach_option(),
11990
12040
  progress_mode: Optional[str] = _progress_option(),
@@ -12310,6 +12360,7 @@ def impact_chain_cmd(
12310
12360
  help="Maximum items per list section (callers, endpoints, security "
12311
12361
  "surfaces). Totals stay exact in metadata/truncated_lists. "
12312
12362
  "0 disables the cap.",
12363
+ min=0,
12313
12364
  ),
12314
12365
  with_findings: bool = typer.Option(
12315
12366
  False, "--with-findings",
@@ -12633,7 +12684,8 @@ def explain_cmd(
12633
12684
  limit: int = typer.Option(
12634
12685
  50, "--limit",
12635
12686
  help="Maximum items per list section (callers, methods, deps). "
12636
- "0 disables the cap. Truncation is always reported in warnings.",
12687
+ "0 disables the cap. Truncation is always reported in warnings.",
12688
+ min=0,
12637
12689
  ),
12638
12690
  copy: bool = _copy_option(),
12639
12691
  progress_mode: Optional[str] = _progress_option(),
@@ -13499,10 +13551,12 @@ def chunk_file_cmd(
13499
13551
  max_lines: int = typer.Option(
13500
13552
  500, "--max-lines", "-n",
13501
13553
  help="Target max lines per chunk (default: 500). Methods > max_lines emit size_warning.",
13554
+ min=1,
13502
13555
  ),
13503
13556
  chunk_id: Optional[int] = typer.Option(
13504
13557
  None, "--chunk", "-c",
13505
13558
  help="Return only this chunk by ID (1-based). Omit to return all chunks.",
13559
+ min=1,
13506
13560
  ),
13507
13561
  metadata_only: bool = typer.Option(
13508
13562
  False, "--metadata-only",
@@ -14190,7 +14244,7 @@ def config_cmd(
14190
14244
  _answer = _TextAnswer(output_path)
14191
14245
  _answer.say(f"ask {__version__}")
14192
14246
  _answer.say(f"Config: {config_file_path()}")
14193
- _answer.say(f"Telemetry: {'enabled' if is_enabled() else 'disabled'} (off by default; opt-in)")
14247
+ _answer.say(f"Telemetry: {'enabled' if is_enabled() else 'disabled'} (on by default; disable with `ask telemetry disable`)")
14194
14248
  _answer.say("")
14195
14249
 
14196
14250
  # F-AX: the declaration may sit outside the analysed tree, and *which* file
@@ -16473,7 +16527,7 @@ def main_entry() -> None:
16473
16527
  INVALID_INPUT_CODE,
16474
16528
  str(_refused),
16475
16529
  hint=(
16476
- "The run was asked to create something inside the repository while "
16530
+ "The run was asked to modify something inside the repository while "
16477
16531
  "--no-write (ASK_READONLY=1) was in force. Drop the flag that "
16478
16532
  "persists — or point its directory option outside the repository — "
16479
16533
  "and the same answer is produced with the artefact."
sourcecode/compare.py CHANGED
@@ -40,7 +40,7 @@ COMPARE_SCHEMA: str = "candidate-comparison-v1"
40
40
  _COST_FIELDS: tuple[str, ...] = (
41
41
  "blast_radius", "affected_endpoints", "modules_spanned",
42
42
  "secured_endpoints", "transactional_boundaries", "tests_at_risk",
43
- "rollback_files",
43
+ "rollback_files", "container_wired_components",
44
44
  )
45
45
 
46
46
  # Resolutions on which D3's build_change_plan returns a bare resolution notice
@@ -108,6 +108,7 @@ def _cost_vector(plan: dict, blast: dict) -> dict[str, int]:
108
108
  "modules_spanned": modules,
109
109
  "secured_endpoints": secured,
110
110
  "transactional_boundaries": txn,
111
+ "container_wired_components": len(plan.get("container_wired") or []),
111
112
  }
112
113
 
113
114
 
@@ -151,6 +152,7 @@ def _rank_key(cand: dict) -> tuple:
151
152
  -c["transactional_boundaries"],
152
153
  -c["tests_at_risk"],
153
154
  -c["rollback_files"],
155
+ -c["container_wired_components"],
154
156
  cand["target"],
155
157
  )
156
158
 
@@ -299,6 +301,10 @@ def build_comparison(
299
301
  ),
300
302
  "tests_at_risk": "covering test symbols that reach the change",
301
303
  "rollback_files": "files the change would touch on revert",
304
+ "container_wired_components": (
305
+ "components reached by declared framework/container wiring; this is "
306
+ "separate from reverse-call fan-in"
307
+ ),
302
308
  },
303
309
  "candidates": candidates,
304
310
  "provenance": (
@@ -145,6 +145,25 @@ class Scope:
145
145
  )
146
146
  return f"at least {count} Java files{cap}, {state}"
147
147
 
148
+ def describe_for_budget(self) -> str:
149
+ """Describe scope without turning partial cache evidence into "warm".
150
+
151
+ The fast scope probe intentionally checks only the per-repository
152
+ snapshot/RIS store; probing the exact shared CIR and parse entries would
153
+ require the same analysed file set and cache key as the command about to
154
+ run. Budget advice therefore names what it did inspect and leaves the
155
+ other two layers undecided. The command payload publishes their actual
156
+ post-lookup state as ``cache_layers``.
157
+ """
158
+ if self.java_files is None:
159
+ return self.describe()
160
+ prefix = self.describe().rsplit(", ", 1)[0]
161
+ snapshot_ris = "present" if self.warm else "absent"
162
+ return (
163
+ f"{prefix}, cache layers: snapshot/RIS {snapshot_ris}; "
164
+ "context/parse unknown before analysis"
165
+ )
166
+
148
167
 
149
168
  def count_java_files(root: Path, *, cap: int = _SCAN_ENTRY_CAP) -> "tuple[Optional[int], bool]":
150
169
  """(count, exact) for the Java files under *root*. Never raises.
sourcecode/mcp/runner.py CHANGED
@@ -7,11 +7,15 @@ lookup, no process fork, no stdout encoding issues.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
+ import threading
10
11
  from typing import Any
11
12
 
12
13
  from typer.testing import CliRunner
13
14
 
14
15
  _runner = CliRunner()
16
+ # CliRunner temporarily replaces process-global stdout/stderr. MCP dispatches
17
+ # tools concurrently, so every in-process invocation must share this lock.
18
+ _runner_lock = threading.RLock()
15
19
 
16
20
 
17
21
  class CommandError(RuntimeError):
@@ -44,7 +48,8 @@ def run_command(args: list[str]) -> Any:
44
48
  # Pass raw args to invoke — the _cmd_main hook inside cli.py handles path
45
49
  # extraction via _preprocess_args. Pre-processing here would strip the path
46
50
  # from args, then _cmd_main would re-process the stripped list and lose it.
47
- result = _runner.invoke(app, list(args))
51
+ with _runner_lock:
52
+ result = _runner.invoke(app, list(args))
48
53
 
49
54
  if result.exit_code != 0:
50
55
  stdout_raw = getattr(result, "output", "")