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 +1 -1
- sourcecode/_docs/DEFECT-LEDGER.md +37 -11
- sourcecode/_docs/USER_GUIDE.md +3 -3
- sourcecode/change_plan.py +8 -1
- sourcecode/cli.py +118 -64
- sourcecode/compare.py +7 -1
- sourcecode/execution_plan.py +19 -0
- sourcecode/mcp/runner.py +6 -1
- sourcecode/mcp/server.py +3 -4
- sourcecode/migrate_check.py +16 -7
- sourcecode/posture.py +52 -9
- sourcecode/readonly.py +32 -8
- sourcecode/release_info.py +1 -1
- sourcecode/rename_refactor.py +14 -4
- sourcecode/repository_ir.py +42 -14
- sourcecode/retrieval/steps_impact.py +1 -1
- sourcecode/risk.py +13 -0
- sourcecode/schema.py +6 -3
- sourcecode/security_posture.py +9 -1
- sourcecode/selftest.py +46 -7
- sourcecode/serializer.py +13 -4
- sourcecode/spring_model.py +13 -0
- sourcecode/summarizer.py +54 -12
- sourcecode/telemetry/__init__.py +5 -4
- sourcecode/telemetry/config.py +14 -19
- sourcecode/telemetry/consent.py +11 -15
- sourcecode/telemetry/events.py +3 -1
- sourcecode/telemetry/filters.py +10 -0
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/METADATA +9 -11
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/RECORD +34 -34
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.12.dist-info → sourcecode-5.8.14.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -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-
|
|
20
|
-
the
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
34
|
-
| `AUD-
|
|
35
|
-
| `AUD-
|
|
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
|
|
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 |
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
|
@@ -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.
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3087
|
-
|
|
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
|
-
|
|
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.
|
|
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} (
|
|
6495
|
-
#
|
|
6496
|
-
#
|
|
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 —
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
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'} (
|
|
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
|
|
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": (
|
sourcecode/execution_plan.py
CHANGED
|
@@ -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
|
-
|
|
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", "")
|