sourcecode 3.6.0__py3-none-any.whl → 3.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/change_plan.py +5 -1
- sourcecode/cli.py +214 -79
- sourcecode/compare.py +5 -3
- sourcecode/explain.py +11 -0
- sourcecode/output_budget.py +7 -2
- sourcecode/pipe_contract.py +208 -0
- sourcecode/repository_ir.py +18 -0
- sourcecode/serializer.py +93 -61
- sourcecode/spring_findings.py +86 -0
- sourcecode/spring_impact.py +18 -1
- sourcecode/target_admission.py +168 -0
- sourcecode/verify_edit.py +32 -9
- {sourcecode-3.6.0.dist-info → sourcecode-3.7.0.dist-info}/METADATA +1 -1
- {sourcecode-3.6.0.dist-info → sourcecode-3.7.0.dist-info}/RECORD +18 -16
- {sourcecode-3.6.0.dist-info → sourcecode-3.7.0.dist-info}/WHEEL +0 -0
- {sourcecode-3.6.0.dist-info → sourcecode-3.7.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-3.6.0.dist-info → sourcecode-3.7.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/change_plan.py
CHANGED
|
@@ -88,7 +88,11 @@ def build_change_plan(
|
|
|
88
88
|
resolution = blast.get("resolution", "not_found")
|
|
89
89
|
|
|
90
90
|
# Unresolvable target → return a plan-shaped resolution notice (never crash).
|
|
91
|
-
|
|
91
|
+
# The set of resolutions that means "never found it" is one fact, and the CLI
|
|
92
|
+
# reads the same one to decide the exit code (C3-29).
|
|
93
|
+
from sourcecode.target_admission import is_unresolved
|
|
94
|
+
|
|
95
|
+
if is_unresolved(resolution) or not blast.get("matched_fqns"):
|
|
92
96
|
return {
|
|
93
97
|
"schema": CHANGE_PLAN_SCHEMA,
|
|
94
98
|
"target": target,
|
sourcecode/cli.py
CHANGED
|
@@ -238,6 +238,64 @@ def _tier_help_block() -> str:
|
|
|
238
238
|
return "\n".join(lines)
|
|
239
239
|
|
|
240
240
|
|
|
241
|
+
#: Command groups whose subcommands `--help` names one by one. A group that is
|
|
242
|
+
#: not listed here still appears — as its exact subcommand count and a pointer to
|
|
243
|
+
#: its own `--help` — so nothing can go missing; `retrieve` publishes 29 parked
|
|
244
|
+
#: intents, and naming them here would bury the blocks a reader acts on.
|
|
245
|
+
_GROUPS_LISTED_IN_HELP = ("cache", "auth", "mcp", "telemetry", "baseline")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _subcommand_summary(entry: Any) -> str:
|
|
249
|
+
"""The one-line description a subcommand publishes about itself."""
|
|
250
|
+
text = str(getattr(entry, "short_help", None) or getattr(entry, "help", None) or "")
|
|
251
|
+
if not text:
|
|
252
|
+
callback = getattr(entry, "callback", None)
|
|
253
|
+
text = str(getattr(callback, "__doc__", "") or "")
|
|
254
|
+
line = text.strip().splitlines()[0].strip() if text.strip() else ""
|
|
255
|
+
line = line.rstrip(".")
|
|
256
|
+
# Kept short so the block still fits an 80-column terminal, where a clipped
|
|
257
|
+
# line would hide the very fact this block exists to publish.
|
|
258
|
+
return line if len(line) <= 46 else line[:45].rstrip() + "…"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _group_help_block() -> str:
|
|
262
|
+
"""The command groups as `--help` prints them — generated from the registered
|
|
263
|
+
groups, never typed out.
|
|
264
|
+
|
|
265
|
+
C4-1 and C4-2 replaced the hand-written catalogue at the *root*, and the same
|
|
266
|
+
omission survived one level down (C4-7): `cache model` — the per-layer answer
|
|
267
|
+
to "what does a warm buy this command" — was in no help text at all, because
|
|
268
|
+
the Cache block was three lines somebody wrote by hand and never revisited.
|
|
269
|
+
A group's subcommands are therefore read from the registry: a new one appears
|
|
270
|
+
in `--help` the moment it is registered, or the group states its own count.
|
|
271
|
+
"""
|
|
272
|
+
registered = globals().get("app")
|
|
273
|
+
groups = list(getattr(registered, "registered_groups", ()) or ())
|
|
274
|
+
if not groups:
|
|
275
|
+
# Import-time call, before any group is registered. The real `--help`
|
|
276
|
+
# render refreshes this text (see `_get_command_with_preprocessing`).
|
|
277
|
+
return ""
|
|
278
|
+
|
|
279
|
+
lines = ["[bold]Command groups:[/bold]"]
|
|
280
|
+
for group in groups:
|
|
281
|
+
name = str(getattr(group, "name", "") or "")
|
|
282
|
+
subcommands = list(
|
|
283
|
+
getattr(getattr(group, "typer_instance", None), "registered_commands", ()) or ()
|
|
284
|
+
)
|
|
285
|
+
if name in _GROUPS_LISTED_IN_HELP:
|
|
286
|
+
for sub in subcommands:
|
|
287
|
+
label = f"{name} {sub.name}"
|
|
288
|
+
lines.append(f" {label:<29}[dim]# {_subcommand_summary(sub)}[/dim]")
|
|
289
|
+
else:
|
|
290
|
+
tier = command_tier(name)
|
|
291
|
+
_tier_note = f", {tier}" if tier in ("experimental", "parked") else ""
|
|
292
|
+
lines.append(
|
|
293
|
+
f" {name + ' <subcommand>':<29}"
|
|
294
|
+
f"[dim]# {len(subcommands)} subcommands{_tier_note} — ask {name} --help[/dim]"
|
|
295
|
+
)
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
|
|
241
299
|
def _build_help_text() -> str:
|
|
242
300
|
"""Build --help text dynamically based on current license state."""
|
|
243
301
|
try:
|
|
@@ -284,16 +342,7 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
|
|
|
284
342
|
|
|
285
343
|
{_tier_help_block()}
|
|
286
344
|
|
|
287
|
-
|
|
288
|
-
auth status [dim]# show current plan and auth state[/dim]
|
|
289
|
-
auth logout [dim]# remove local credentials[/dim]
|
|
290
|
-
|
|
291
|
-
[bold]Cache commands:[/bold]
|
|
292
|
-
cache status [dim]# cache size, hit keys, last-warmed timestamp[/dim]
|
|
293
|
-
cache warm [dim]# pre-build structural layers + compact view
|
|
294
|
-
# (--agent warms the agent view too; --full,
|
|
295
|
-
# --env-map and raised --depth are NOT warmed)[/dim]
|
|
296
|
-
cache clear [dim]# clear all cached results for this repo[/dim]
|
|
345
|
+
{_group_help_block()}
|
|
297
346
|
|
|
298
347
|
[bold]Examples:[/bold]
|
|
299
348
|
ask posture . --diff default:prod -o posture.json
|
|
@@ -305,12 +354,6 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
|
|
|
305
354
|
|
|
306
355
|
[bold]Subcommands:[/bold]
|
|
307
356
|
prepare-context TASK [PATH] [dim]# task-specific context (onboard, delta, fix-bug, ...)[/dim]
|
|
308
|
-
mcp init [dim]# setup MCP integration (Claude Desktop, Cursor)[/dim]
|
|
309
|
-
mcp status [dim]# show MCP integration status[/dim]
|
|
310
|
-
mcp remove [dim]# remove MCP integration safely[/dim]
|
|
311
|
-
mcp serve [dim]# start MCP server for AI agent integration[/dim]
|
|
312
|
-
telemetry status|enable|disable
|
|
313
|
-
version
|
|
314
357
|
"""
|
|
315
358
|
|
|
316
359
|
if not _is_pro:
|
|
@@ -674,6 +717,53 @@ def _notice(message: str) -> None:
|
|
|
674
717
|
typer.echo(message, err=True)
|
|
675
718
|
|
|
676
719
|
|
|
720
|
+
def _apply_budget(
|
|
721
|
+
data: dict,
|
|
722
|
+
budget_bytes: int,
|
|
723
|
+
*,
|
|
724
|
+
label: str,
|
|
725
|
+
output_path: "Optional[Path]",
|
|
726
|
+
) -> dict:
|
|
727
|
+
"""Apply the payload budget — the one seam every command goes through.
|
|
728
|
+
|
|
729
|
+
The budget exists because an agent's context window is finite, so it is a
|
|
730
|
+
constraint on **stdout** and on nothing else. When `--output FILE` is given
|
|
731
|
+
the payload does not pass through a context window at all, and cutting it
|
|
732
|
+
there breaks the contract twice over: the answer is silently sampled, and
|
|
733
|
+
the note explaining the cut advises `--output` — the flag already in use
|
|
734
|
+
(C3-17). Two of the six call sites had the rule and four did not, which is
|
|
735
|
+
the shape this seam removes: one place decides, every command inherits it.
|
|
736
|
+
"""
|
|
737
|
+
from sourcecode.output_budget import trim_to_budget as _trim
|
|
738
|
+
|
|
739
|
+
to_file = output_path is not None
|
|
740
|
+
return _trim(
|
|
741
|
+
data, budget_bytes, label=label, skip=to_file, warn_stderr=not to_file
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _prepare_context_budget(task: str) -> int:
|
|
746
|
+
"""The stdout budget for a `prepare-context` task — one table, two readers.
|
|
747
|
+
|
|
748
|
+
The fresh path and the cache-hit path both budget the payload, and a second
|
|
749
|
+
copy of this mapping beside the first is exactly how the two drift apart.
|
|
750
|
+
"""
|
|
751
|
+
from sourcecode.output_budget import (
|
|
752
|
+
BUDGET_DELTA, BUDGET_EXPLAIN, BUDGET_FIX_BUG,
|
|
753
|
+
BUDGET_ONBOARD, BUDGET_REFACTOR, BUDGET_REVIEW_PR,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"fix-bug": BUDGET_FIX_BUG,
|
|
758
|
+
"review-pr": BUDGET_REVIEW_PR,
|
|
759
|
+
"onboard": BUDGET_ONBOARD,
|
|
760
|
+
"explain": BUDGET_EXPLAIN,
|
|
761
|
+
"refactor": BUDGET_REFACTOR,
|
|
762
|
+
"delta": BUDGET_DELTA,
|
|
763
|
+
"generate-tests": BUDGET_EXPLAIN,
|
|
764
|
+
}.get(task, BUDGET_EXPLAIN)
|
|
765
|
+
|
|
766
|
+
|
|
677
767
|
def _admit_path(
|
|
678
768
|
raw: "Path | str",
|
|
679
769
|
*,
|
|
@@ -713,6 +803,21 @@ def _admit_path(
|
|
|
713
803
|
raise typer.Exit(code=exit_code)
|
|
714
804
|
|
|
715
805
|
|
|
806
|
+
def _exit_on_unresolved_target(resolution: "Optional[str]") -> None:
|
|
807
|
+
"""Close the run on a target the analysis never resolved — one exit code.
|
|
808
|
+
|
|
809
|
+
The payload has already been emitted and it is truthful: it names the
|
|
810
|
+
resolution and lists nothing. What was inconsistent is the code a pipeline
|
|
811
|
+
reads — `impact` said 1, `plan` said 0 over the same fact (C3-29) — so the
|
|
812
|
+
decision belongs to `target_admission`, not to each command.
|
|
813
|
+
"""
|
|
814
|
+
from sourcecode.target_admission import exit_code_for
|
|
815
|
+
|
|
816
|
+
code = exit_code_for(resolution)
|
|
817
|
+
if code:
|
|
818
|
+
raise typer.Exit(code=code)
|
|
819
|
+
|
|
820
|
+
|
|
716
821
|
def _misread_path_note(symbol: str, analysed_root: "Path", usage: str) -> "Optional[str]":
|
|
717
822
|
"""State the repository actually analysed when the symbol argument holds a path.
|
|
718
823
|
|
|
@@ -1518,7 +1623,11 @@ def main(
|
|
|
1518
1623
|
full: bool = typer.Option(
|
|
1519
1624
|
False,
|
|
1520
1625
|
"--full",
|
|
1521
|
-
help=
|
|
1626
|
+
help=(
|
|
1627
|
+
"Raise the display caps: transactional_boundaries and mybatis.dto_mappers "
|
|
1628
|
+
"are listed in full, file_relevance rises to 40 entries. Other caps stay, "
|
|
1629
|
+
"and each says what it cut; --output FILE skips the payload budget entirely."
|
|
1630
|
+
),
|
|
1522
1631
|
),
|
|
1523
1632
|
trace_pipeline: bool = typer.Option(
|
|
1524
1633
|
False,
|
|
@@ -2146,17 +2255,15 @@ def main(
|
|
|
2146
2255
|
_rebuilt = _red_l1(_rebuilt)
|
|
2147
2256
|
# Apply output budget
|
|
2148
2257
|
if agent:
|
|
2149
|
-
from sourcecode.output_budget import
|
|
2150
|
-
|
|
2151
|
-
BUDGET_AGENT,
|
|
2258
|
+
from sourcecode.output_budget import BUDGET_AGENT
|
|
2259
|
+
_rebuilt = _apply_budget(
|
|
2260
|
+
_rebuilt, BUDGET_AGENT, label="agent", output_path=output
|
|
2152
2261
|
)
|
|
2153
|
-
_rebuilt = _trim_l1(_rebuilt, BUDGET_AGENT, label="agent")
|
|
2154
2262
|
elif compact:
|
|
2155
|
-
from sourcecode.output_budget import
|
|
2156
|
-
|
|
2157
|
-
BUDGET_COMPACT,
|
|
2263
|
+
from sourcecode.output_budget import BUDGET_COMPACT
|
|
2264
|
+
_rebuilt = _apply_budget(
|
|
2265
|
+
_rebuilt, BUDGET_COMPACT, label="compact", output_path=output
|
|
2158
2266
|
)
|
|
2159
|
-
_rebuilt = _trim_l1c(_rebuilt, BUDGET_COMPACT, label="compact")
|
|
2160
2267
|
# Serialize
|
|
2161
2268
|
_cache_hit_content = _serialize_dict(_rebuilt, format)
|
|
2162
2269
|
# Cache rebuilt view in L2 (skip for --changed-only: stale diff)
|
|
@@ -2315,11 +2422,12 @@ def main(
|
|
|
2315
2422
|
# IMP-2: warn if the exclude value looks like it was swallowed as a path
|
|
2316
2423
|
# (BUG-2 symptom in older versions: --exclude value consumed as repo path).
|
|
2317
2424
|
if len(_extra_excludes) == 1 and Path(list(_extra_excludes)[0]).is_dir():
|
|
2318
|
-
|
|
2425
|
+
# C3-34: same rule as every other advisory — the run succeeds, so
|
|
2426
|
+
# this speaks to a person at a terminal and never to a pipe.
|
|
2427
|
+
_notice(
|
|
2319
2428
|
f"[sourcecode] Warning: --exclude value '{list(_extra_excludes)[0]}' is a directory path. "
|
|
2320
|
-
"If this was meant as a pattern, use --exclude=pattern or --exclude pattern (both are supported)
|
|
2429
|
+
"If this was meant as a pattern, use --exclude=pattern or --exclude pattern (both are supported)."
|
|
2321
2430
|
)
|
|
2322
|
-
sys.stderr.flush()
|
|
2323
2431
|
|
|
2324
2432
|
_progress = Progress()
|
|
2325
2433
|
_progress.start("scanning files")
|
|
@@ -3065,8 +3173,8 @@ def main(
|
|
|
3065
3173
|
data = redact_dict(data)
|
|
3066
3174
|
# P0-1: Apply output budget — safety net for large repos.
|
|
3067
3175
|
# Skip budget when writing to a file (no size constraint); warn on stdout.
|
|
3068
|
-
from sourcecode.output_budget import
|
|
3069
|
-
data =
|
|
3176
|
+
from sourcecode.output_budget import BUDGET_AGENT
|
|
3177
|
+
data = _apply_budget(data, BUDGET_AGENT, label="agent", output_path=output)
|
|
3070
3178
|
# FIX-P0-2: agent mode must honour --format yaml (previously always emitted JSON).
|
|
3071
3179
|
content = _serialize_dict(data, format)
|
|
3072
3180
|
elif compact:
|
|
@@ -3087,8 +3195,8 @@ def main(
|
|
|
3087
3195
|
data = redact_dict(data)
|
|
3088
3196
|
# P0-1: Apply output budget — safety net for large repos.
|
|
3089
3197
|
# Skip budget when writing to a file (no size constraint); warn on stdout.
|
|
3090
|
-
from sourcecode.output_budget import
|
|
3091
|
-
data =
|
|
3198
|
+
from sourcecode.output_budget import BUDGET_COMPACT
|
|
3199
|
+
data = _apply_budget(data, BUDGET_COMPACT, label="compact", output_path=output)
|
|
3092
3200
|
content = _serialize_dict(data, format)
|
|
3093
3201
|
else:
|
|
3094
3202
|
raw_dict = standard_view(sm, include_tree=tree and not no_tree)
|
|
@@ -3605,6 +3713,22 @@ def prepare_context_cmd(
|
|
|
3605
3713
|
_pctx_cache_key = f"pctx-{task}-{_pctx_git_sha}-{_sym_h}-{format or 'json'}"
|
|
3606
3714
|
_cached_pctx = _pctx_cache.read(target, _pctx_cache_key)
|
|
3607
3715
|
if _cached_pctx is not None:
|
|
3716
|
+
# Cached documents are stored pre-budget: a hit that reused the
|
|
3717
|
+
# previous run's trimming would send a stdout-sized answer to a
|
|
3718
|
+
# file, which is the defect the budget seam exists to stop (C3-17).
|
|
3719
|
+
try:
|
|
3720
|
+
_cached_pctx = json.dumps(
|
|
3721
|
+
_apply_budget(
|
|
3722
|
+
json.loads(_cached_pctx),
|
|
3723
|
+
_prepare_context_budget(task),
|
|
3724
|
+
label=task,
|
|
3725
|
+
output_path=output_path,
|
|
3726
|
+
),
|
|
3727
|
+
indent=2,
|
|
3728
|
+
ensure_ascii=False,
|
|
3729
|
+
)
|
|
3730
|
+
except Exception:
|
|
3731
|
+
pass # not JSON (or unreadable) → serve exactly what was stored
|
|
3608
3732
|
_emit_command_output(_cached_pctx, output_path, copy)
|
|
3609
3733
|
try:
|
|
3610
3734
|
from sourcecode import telemetry as _tel
|
|
@@ -3958,22 +4082,7 @@ def prepare_context_cmd(
|
|
|
3958
4082
|
out["skipped_analyzers"] = _skipped
|
|
3959
4083
|
|
|
3960
4084
|
# P0-1: Apply output budget per task — safety net for large repos.
|
|
3961
|
-
|
|
3962
|
-
trim_to_budget as _pc_trim,
|
|
3963
|
-
BUDGET_FIX_BUG, BUDGET_REVIEW_PR, BUDGET_ONBOARD,
|
|
3964
|
-
BUDGET_EXPLAIN, BUDGET_REFACTOR, BUDGET_DELTA,
|
|
3965
|
-
)
|
|
3966
|
-
_pc_budgets: dict[str, int] = {
|
|
3967
|
-
"fix-bug": BUDGET_FIX_BUG,
|
|
3968
|
-
"review-pr": BUDGET_REVIEW_PR,
|
|
3969
|
-
"onboard": BUDGET_ONBOARD,
|
|
3970
|
-
"explain": BUDGET_EXPLAIN,
|
|
3971
|
-
"refactor": BUDGET_REFACTOR,
|
|
3972
|
-
"delta": BUDGET_DELTA,
|
|
3973
|
-
"generate-tests": BUDGET_EXPLAIN,
|
|
3974
|
-
}
|
|
3975
|
-
_pc_budget = _pc_budgets.get(task, BUDGET_EXPLAIN)
|
|
3976
|
-
out = _pc_trim(out, _pc_budget, label=task)
|
|
4085
|
+
_pc_budget = _prepare_context_budget(task)
|
|
3977
4086
|
|
|
3978
4087
|
# Size-gated preview: on enterprise-scale monoliths free users get a capped
|
|
3979
4088
|
# preview (top-5 / lightweight) + upgrade note. Small/mid repos get the full
|
|
@@ -4002,6 +4111,13 @@ def prepare_context_cmd(
|
|
|
4002
4111
|
"complete execution paths, and CI-grade risk scoring."
|
|
4003
4112
|
)
|
|
4004
4113
|
|
|
4114
|
+
# The cache stores the analysis, never the rendering. The budget depends on
|
|
4115
|
+
# where this particular run sends its payload (C3-17), so caching the trimmed
|
|
4116
|
+
# document would serve a stdout-sized answer to the next `--output FILE` run —
|
|
4117
|
+
# the defect surviving one layer down. Written pre-budget, budgeted per run.
|
|
4118
|
+
_pc_cacheable_content = json.dumps(out, indent=2, ensure_ascii=False)
|
|
4119
|
+
out = _apply_budget(out, _pc_budget, label=task, output_path=output_path)
|
|
4120
|
+
|
|
4005
4121
|
if format == "github-comment" and task == "review-pr":
|
|
4006
4122
|
from sourcecode.pr_comment_renderer import render_github_comment
|
|
4007
4123
|
_pc_content = render_github_comment(out)
|
|
@@ -4010,7 +4126,7 @@ def prepare_context_cmd(
|
|
|
4010
4126
|
|
|
4011
4127
|
if _pctx_cacheable and _pctx_cache_key and format != "github-comment":
|
|
4012
4128
|
try:
|
|
4013
|
-
_pctx_cache.write(target, _pctx_cache_key,
|
|
4129
|
+
_pctx_cache.write(target, _pctx_cache_key, _pc_cacheable_content)
|
|
4014
4130
|
except Exception:
|
|
4015
4131
|
pass
|
|
4016
4132
|
|
|
@@ -4330,17 +4446,17 @@ def repo_ir_cmd(
|
|
|
4330
4446
|
)
|
|
4331
4447
|
raise typer.Exit(1)
|
|
4332
4448
|
if _ir_tokens_est > 10_000:
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4449
|
+
# C3-34: the run succeeded and the IR is on stdout, so this advisory
|
|
4450
|
+
# is a note to a person — on a pipe it is noise, and on PowerShell
|
|
4451
|
+
# 5.1 it turns a successful `ask repo-ir --force > ir.json` into a
|
|
4452
|
+
# NativeCommandError. Terminal-gated like every other notice.
|
|
4453
|
+
_smaller = (
|
|
4454
|
+
"--max-nodes N --max-edges N" if summary_only else "--summary-only"
|
|
4455
|
+
)
|
|
4456
|
+
_notice(
|
|
4457
|
+
f"[repo-ir] ~{_ir_tokens_est // 1000}K tokens — "
|
|
4458
|
+
f"use {_smaller} or --output FILE for smaller output."
|
|
4459
|
+
)
|
|
4344
4460
|
_emit_command_output(output, None, copy)
|
|
4345
4461
|
|
|
4346
4462
|
|
|
@@ -4428,7 +4544,7 @@ def impact_cmd(
|
|
|
4428
4544
|
from sourcecode.repository_ir import (
|
|
4429
4545
|
build_repo_ir, find_java_files, compute_blast_radius,
|
|
4430
4546
|
)
|
|
4431
|
-
from sourcecode.output_budget import
|
|
4547
|
+
from sourcecode.output_budget import BUDGET_IMPACT
|
|
4432
4548
|
|
|
4433
4549
|
# Legacy-compat: old syntax was `impact <path> <target>`.
|
|
4434
4550
|
# Detect: target resolves to an existing directory (not a class name), and
|
|
@@ -4495,7 +4611,7 @@ def impact_cmd(
|
|
|
4495
4611
|
result = compute_blast_radius(
|
|
4496
4612
|
ir, target, max_depth=depth, list_limit=0 if _to_file else None
|
|
4497
4613
|
)
|
|
4498
|
-
result =
|
|
4614
|
+
result = _apply_budget(result, BUDGET_IMPACT, label="impact", output_path=output_path)
|
|
4499
4615
|
if _misread and result.get("resolution") == "not_found":
|
|
4500
4616
|
result["analysis_warnings"] = [*(result.get("analysis_warnings") or []), _misread]
|
|
4501
4617
|
|
|
@@ -4503,8 +4619,7 @@ def impact_cmd(
|
|
|
4503
4619
|
_emit_command_output(output, output_path, copy,
|
|
4504
4620
|
success_msg=f"Impact analysis written to {output_path}")
|
|
4505
4621
|
|
|
4506
|
-
|
|
4507
|
-
raise typer.Exit(code=1)
|
|
4622
|
+
_exit_on_unresolved_target(result.get("resolution"))
|
|
4508
4623
|
|
|
4509
4624
|
from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
|
|
4510
4625
|
_nudge()
|
|
@@ -5545,9 +5660,6 @@ def plan_cmd(
|
|
|
5545
5660
|
data = build_change_plan(_cir, target, max_depth=depth)
|
|
5546
5661
|
_prog.finish()
|
|
5547
5662
|
|
|
5548
|
-
if data.get("resolution") in ("not_found", "ambiguous_path"):
|
|
5549
|
-
typer.echo(f"Note: {data.get('message', 'target not resolved')}", err=True)
|
|
5550
|
-
|
|
5551
5663
|
output = _serialize_dict(data, format)
|
|
5552
5664
|
_emit_command_output(
|
|
5553
5665
|
output, output_path, copy,
|
|
@@ -5559,6 +5671,12 @@ def plan_cmd(
|
|
|
5559
5671
|
),
|
|
5560
5672
|
)
|
|
5561
5673
|
|
|
5674
|
+
# C3-29: the payload already names the resolution and lists nothing; the
|
|
5675
|
+
# `Note:` on stderr that used to stand here said the same thing a second
|
|
5676
|
+
# time on a run that exited 0, so a gate reading the exit code was told the
|
|
5677
|
+
# plan succeeded. The exit code now carries it, and stderr stays clean.
|
|
5678
|
+
_exit_on_unresolved_target(data.get("resolution"))
|
|
5679
|
+
|
|
5562
5680
|
|
|
5563
5681
|
# ── D4 Candidate Comparison ───────────────────────────────────────────────────
|
|
5564
5682
|
|
|
@@ -6069,6 +6187,15 @@ def spring_audit_cmd(
|
|
|
6069
6187
|
"--ci/--no-ci",
|
|
6070
6188
|
help="Exit with code 1 if any findings at or above --min-severity are found. For CI/CD gates.",
|
|
6071
6189
|
),
|
|
6190
|
+
compact: bool = typer.Option(
|
|
6191
|
+
False,
|
|
6192
|
+
"--compact",
|
|
6193
|
+
help=(
|
|
6194
|
+
"Bounded summary: caps `findings` and the per-endpoint security projection "
|
|
6195
|
+
"to their top 5, most-severe first. Every count is still measured over the "
|
|
6196
|
+
"whole population. The full report can exceed 1 MB on a monolith."
|
|
6197
|
+
),
|
|
6198
|
+
),
|
|
6072
6199
|
no_cache: bool = typer.Option(
|
|
6073
6200
|
False, "--no-cache",
|
|
6074
6201
|
help=_NO_CACHE_SUBCOMMAND_HELP,
|
|
@@ -6104,6 +6231,7 @@ def spring_audit_cmd(
|
|
|
6104
6231
|
ask spring-audit /path/to/repo
|
|
6105
6232
|
ask spring-audit . --scope security
|
|
6106
6233
|
ask spring-audit . --min-severity high
|
|
6234
|
+
ask spring-audit . --compact
|
|
6107
6235
|
ask spring-audit . --output audit.json
|
|
6108
6236
|
"""
|
|
6109
6237
|
import json as _json
|
|
@@ -6182,7 +6310,7 @@ def spring_audit_cmd(
|
|
|
6182
6310
|
except Exception:
|
|
6183
6311
|
pass
|
|
6184
6312
|
|
|
6185
|
-
data = combined.to_dict()
|
|
6313
|
+
data = combined.to_compact_dict() if compact else combined.to_dict()
|
|
6186
6314
|
|
|
6187
6315
|
# Non-fatal RIS side-effect — persist summary only (not full findings).
|
|
6188
6316
|
try:
|
|
@@ -7100,8 +7228,7 @@ def impact_chain_cmd(
|
|
|
7100
7228
|
),
|
|
7101
7229
|
)
|
|
7102
7230
|
|
|
7103
|
-
|
|
7104
|
-
raise typer.Exit(code=1)
|
|
7231
|
+
_exit_on_unresolved_target(result.resolution)
|
|
7105
7232
|
|
|
7106
7233
|
|
|
7107
7234
|
# ── PR Impact Report ──────────────────────────────────────────────────────────
|
|
@@ -7418,8 +7545,11 @@ def explain_cmd(
|
|
|
7418
7545
|
_emit_command_output(output, output_path, copy,
|
|
7419
7546
|
success_msg=f"Explanation written to {output_path}")
|
|
7420
7547
|
|
|
7421
|
-
|
|
7422
|
-
|
|
7548
|
+
# `explain` reports a flag where the others report a resolution; it maps onto
|
|
7549
|
+
# the same vocabulary rather than deriving its own code beside the authority.
|
|
7550
|
+
from sourcecode.target_admission import resolution_for_found_flag
|
|
7551
|
+
|
|
7552
|
+
_exit_on_unresolved_target(resolution_for_found_flag(explanation.found))
|
|
7423
7553
|
|
|
7424
7554
|
|
|
7425
7555
|
# ── Enterprise Workflow Commands ──────────────────────────────────────────────
|
|
@@ -7722,7 +7852,7 @@ def modernize_cmd(
|
|
|
7722
7852
|
"""
|
|
7723
7853
|
import json as _json
|
|
7724
7854
|
from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
|
|
7725
|
-
from sourcecode.output_budget import
|
|
7855
|
+
from sourcecode.output_budget import BUDGET_ONBOARD
|
|
7726
7856
|
from sourcecode.license import is_pro as _mod_is_pro, is_large_repo as _mod_large
|
|
7727
7857
|
|
|
7728
7858
|
root = _admit_path(path)
|
|
@@ -8027,7 +8157,7 @@ def modernize_cmd(
|
|
|
8027
8157
|
),
|
|
8028
8158
|
}
|
|
8029
8159
|
|
|
8030
|
-
result =
|
|
8160
|
+
result = _apply_budget(result, BUDGET_ONBOARD, label="modernize", output_path=output_path)
|
|
8031
8161
|
output = _json.dumps(result, indent=2, ensure_ascii=False)
|
|
8032
8162
|
|
|
8033
8163
|
if output_path:
|
|
@@ -9570,7 +9700,7 @@ def _warm_shared_cir(target: Path):
|
|
|
9570
9700
|
return None
|
|
9571
9701
|
|
|
9572
9702
|
|
|
9573
|
-
@cache_app.command("warm")
|
|
9703
|
+
@cache_app.command("warm", short_help="Pre-build the cache: structural layers + compact view")
|
|
9574
9704
|
def cache_warm_cmd(
|
|
9575
9705
|
path: Path = typer.Argument(Path("."), help="Repository path to warm (default: current directory)"),
|
|
9576
9706
|
compact: bool = typer.Option(True, "--compact/--no-compact", help="Warm compact view (default: on)."),
|
|
@@ -9662,7 +9792,7 @@ def cache_warm_cmd(
|
|
|
9662
9792
|
)
|
|
9663
9793
|
|
|
9664
9794
|
|
|
9665
|
-
@cache_app.command("model")
|
|
9795
|
+
@cache_app.command("model", short_help="What a warm buys each command, per cache layer")
|
|
9666
9796
|
def cache_model_cmd(
|
|
9667
9797
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
9668
9798
|
markdown: bool = typer.Option(False, "--markdown", help="Output the tables published in the user guide."),
|
|
@@ -9690,7 +9820,7 @@ def cache_model_cmd(
|
|
|
9690
9820
|
typer.echo(_cmodel.render_text())
|
|
9691
9821
|
|
|
9692
9822
|
|
|
9693
|
-
@cache_app.command("freshness")
|
|
9823
|
+
@cache_app.command("freshness", short_help="RIS freshness relative to the current git HEAD")
|
|
9694
9824
|
def cache_freshness_cmd(
|
|
9695
9825
|
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
9696
9826
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
@@ -9772,7 +9902,7 @@ def cache_freshness_cmd(
|
|
|
9772
9902
|
typer.echo(f"RIS updated: {result.get('ris_last_updated_at') or 'never'}")
|
|
9773
9903
|
|
|
9774
9904
|
|
|
9775
|
-
@cache_app.command("context-stats")
|
|
9905
|
+
@cache_app.command("context-stats", short_help="AI context cache: size and reuse metrics")
|
|
9776
9906
|
def cache_context_stats_cmd(
|
|
9777
9907
|
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
9778
9908
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
@@ -9804,7 +9934,7 @@ def cache_context_stats_cmd(
|
|
|
9804
9934
|
typer.echo(f"KL version: {stats['kl_schema_version']} producer={stats['producer_version']}")
|
|
9805
9935
|
|
|
9806
9936
|
|
|
9807
|
-
@cache_app.command("context-clear")
|
|
9937
|
+
@cache_app.command("context-clear", short_help="Delete the AI context cache for a repository")
|
|
9808
9938
|
def cache_context_clear_cmd(
|
|
9809
9939
|
path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
|
|
9810
9940
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
@@ -9929,6 +10059,11 @@ def main_entry() -> None:
|
|
|
9929
10059
|
routing for tokens like 'version' or 'config').
|
|
9930
10060
|
"""
|
|
9931
10061
|
_force_utf8_streams()
|
|
10062
|
+
# A reader that stops reading is not a failed run (C3-28). Installed after
|
|
10063
|
+
# the UTF-8 reconfiguration so the wrapper keeps that stream, and before any
|
|
10064
|
+
# command writes a byte.
|
|
10065
|
+
from sourcecode.pipe_contract import install as _install_pipe_contract
|
|
10066
|
+
_install_pipe_contract()
|
|
9932
10067
|
# Deprecation notice when invoked through the legacy `sourcecode` alias.
|
|
9933
10068
|
# One line, and only on an interactive terminal — pipes/agents that still call
|
|
9934
10069
|
# `sourcecode` keep clean stderr (error envelopes are JSON on stderr), so the
|
sourcecode/compare.py
CHANGED
|
@@ -34,9 +34,11 @@ if TYPE_CHECKING:
|
|
|
34
34
|
COMPARE_SCHEMA: str = "candidate-comparison-v1"
|
|
35
35
|
|
|
36
36
|
# Resolutions on which D3's build_change_plan returns a bare resolution notice
|
|
37
|
-
# (no affected set)
|
|
38
|
-
#
|
|
39
|
-
|
|
37
|
+
# (no affected set), so "resolved" here means "build_change_plan actually
|
|
38
|
+
# produced a plan for this target". Read from the authority rather than
|
|
39
|
+
# restated: this set had been written out three times, and the copy in the CLI
|
|
40
|
+
# is what let `plan` and `impact` disagree about the same fact (C3-29).
|
|
41
|
+
from sourcecode.target_admission import UNRESOLVED_RESOLUTIONS as _UNRESOLVED
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def _measured_count(blast: dict, stat_key: str, list_key: str) -> tuple[int, bool]:
|
sourcecode/explain.py
CHANGED
|
@@ -206,7 +206,18 @@ def _resolve_fqn(class_name: str, cir: "CanonicalRepositoryIR") -> tuple[str, li
|
|
|
206
206
|
|
|
207
207
|
Returns (best_fqn, all_matches).
|
|
208
208
|
best_fqn is empty string when no match found.
|
|
209
|
+
|
|
210
|
+
A target that names a *file* is resolved by that file first (C3-35): the
|
|
211
|
+
path is the more specific input, and this resolver used to answer
|
|
212
|
+
`not_found` for it because no CIR symbol is ever spelled as a path.
|
|
209
213
|
"""
|
|
214
|
+
from sourcecode.target_admission import fqns_declared_in_file, is_path_like
|
|
215
|
+
|
|
216
|
+
if is_path_like(class_name):
|
|
217
|
+
by_file = sorted(fqns_declared_in_file(class_name, _get_raw_nodes(cir)))
|
|
218
|
+
if by_file:
|
|
219
|
+
return by_file[0], by_file
|
|
220
|
+
|
|
210
221
|
suffix_dot = f".{class_name}"
|
|
211
222
|
suffix_hash = f"{class_name}#"
|
|
212
223
|
matches: list[str] = []
|
sourcecode/output_budget.py
CHANGED
|
@@ -253,8 +253,13 @@ def trim_to_budget(
|
|
|
253
253
|
)
|
|
254
254
|
if label:
|
|
255
255
|
_warn_line = f"[{label}] {_warn_line}"
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
# C3-26/C3-34: the run succeeded and the payload is on stdout, so
|
|
257
|
+
# this line speaks to a person at a terminal. On a pipe it is noise
|
|
258
|
+
# that PowerShell 5.1 turns into a NativeCommandError — and the fact
|
|
259
|
+
# itself is already in the payload, under `_truncation_summary`.
|
|
260
|
+
if sys.stderr.isatty():
|
|
261
|
+
sys.stderr.write(_warn_line)
|
|
262
|
+
sys.stderr.flush()
|
|
258
263
|
|
|
259
264
|
return result
|
|
260
265
|
|