sourcecode 2.0.1__py3-none-any.whl → 2.2.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.
- sourcecode/__init__.py +6 -2
- sourcecode/caller_metrics.py +42 -0
- sourcecode/cli.py +325 -207
- sourcecode/detectors/java.py +29 -1
- sourcecode/endpoint_metrics.py +42 -0
- sourcecode/explain.py +2 -0
- sourcecode/license.py +31 -13
- sourcecode/mcp/onboarding/applier.py +14 -6
- sourcecode/mcp/registry.py +22 -22
- sourcecode/mcp/runner.py +2 -2
- sourcecode/mcp/server.py +24 -24
- sourcecode/mcp_nudge.py +1 -1
- sourcecode/path_filters.py +2 -1
- sourcecode/repository_ir.py +51 -15
- sourcecode/ris.py +1 -1
- sourcecode/security_posture.py +608 -0
- sourcecode/semantic_integration_engine.py +18 -2
- sourcecode/serializer.py +24 -4
- sourcecode/spring_findings.py +4 -0
- sourcecode/spring_security_audit.py +31 -0
- sourcecode/spring_semantic.py +18 -5
- sourcecode/spring_tx_analyzer.py +21 -6
- sourcecode/telemetry/consent.py +1 -1
- sourcecode/validation_inference.py +249 -0
- sourcecode-2.2.0.dist-info/METADATA +241 -0
- {sourcecode-2.0.1.dist-info → sourcecode-2.2.0.dist-info}/RECORD +29 -25
- {sourcecode-2.0.1.dist-info → sourcecode-2.2.0.dist-info}/entry_points.txt +1 -0
- sourcecode-2.0.1.dist-info/METADATA +0 -922
- {sourcecode-2.0.1.dist-info → sourcecode-2.2.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.0.1.dist-info → sourcecode-2.2.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/cli.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
import threading
|
|
7
8
|
import time
|
|
@@ -14,6 +15,7 @@ from sourcecode import __version__
|
|
|
14
15
|
from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
15
16
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
16
17
|
from sourcecode.progress import Progress
|
|
18
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
17
19
|
from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
|
|
18
20
|
|
|
19
21
|
|
|
@@ -152,10 +154,10 @@ def _build_help_text() -> str:
|
|
|
152
154
|
if _is_pro:
|
|
153
155
|
plan_badge = "[bold green]● Pro[/bold green]"
|
|
154
156
|
else:
|
|
155
|
-
plan_badge = "[yellow]Free[/yellow] · [dim]
|
|
157
|
+
plan_badge = "[yellow]Free[/yellow] · [dim]ask activate <key>[/dim] to unlock Pro"
|
|
156
158
|
|
|
157
159
|
text = f"""\
|
|
158
|
-
[bold]ASK Engine[/bold] [dim]· CLI:
|
|
160
|
+
[bold]ASK Engine[/bold] [dim]· CLI: ask[/dim] {plan_badge}
|
|
159
161
|
|
|
160
162
|
Persistent structural context and ultra-fast repeated analysis for AI coding agents.
|
|
161
163
|
|
|
@@ -163,9 +165,9 @@ Cache warms on first scan; every subsequent call returns pre-built context in mi
|
|
|
163
165
|
Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
|
|
164
166
|
|
|
165
167
|
[bold]Primary usage:[/bold]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
ask --compact high-signal summary (~2,500–4,000 tokens)
|
|
169
|
+
ask --compact --git-context include git hotspots and uncommitted files
|
|
170
|
+
ask --agent full structured JSON for AI agents
|
|
169
171
|
|
|
170
172
|
[bold]Auth commands:[/bold]
|
|
171
173
|
auth status [dim]# show current plan and auth state[/dim]
|
|
@@ -177,11 +179,11 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
|
|
|
177
179
|
cache clear [dim]# clear all cached results for this repo[/dim]
|
|
178
180
|
|
|
179
181
|
[bold]Examples:[/bold]
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
ask my-project --compact
|
|
183
|
+
ask . --compact --git-context --copy
|
|
184
|
+
ask . --changed-only --git-context
|
|
185
|
+
ask prepare-context onboard my-project
|
|
186
|
+
ask prepare-context delta . --since main
|
|
185
187
|
|
|
186
188
|
[bold]Subcommands:[/bold]
|
|
187
189
|
prepare-context TASK [PATH] [dim]# task-specific context (onboard, delta, fix-bug, ...)[/dim]
|
|
@@ -206,7 +208,7 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
|
|
|
206
208
|
|
|
207
209
|
[dim]Non-Java repos are free at any size. Local MCP serve is free.[/dim]
|
|
208
210
|
|
|
209
|
-
[dim cyan]→
|
|
211
|
+
[dim cyan]→ ask activate <key>[/dim cyan]
|
|
210
212
|
"""
|
|
211
213
|
|
|
212
214
|
return text
|
|
@@ -523,7 +525,7 @@ def _copy_to_clipboard(content: str) -> bool:
|
|
|
523
525
|
|
|
524
526
|
|
|
525
527
|
app = typer.Typer(
|
|
526
|
-
name="
|
|
528
|
+
name="ask",
|
|
527
529
|
help=_HELP,
|
|
528
530
|
add_completion=False,
|
|
529
531
|
rich_markup_mode="rich",
|
|
@@ -624,7 +626,7 @@ def _maybe_show_mcp_hint() -> None:
|
|
|
624
626
|
return
|
|
625
627
|
typer.echo("", err=True)
|
|
626
628
|
typer.echo(" MCP integration available:", err=True)
|
|
627
|
-
typer.echo(" →
|
|
629
|
+
typer.echo(" → ask mcp init", err=True)
|
|
628
630
|
typer.echo("", err=True)
|
|
629
631
|
data.setdefault("mcp", {})["hint_shown"] = True
|
|
630
632
|
_save(data)
|
|
@@ -673,47 +675,41 @@ _IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
|
|
|
673
675
|
|
|
674
676
|
def version_callback(value: bool) -> None:
|
|
675
677
|
if value:
|
|
676
|
-
typer.echo(f"
|
|
678
|
+
typer.echo(f"ask {__version__}")
|
|
677
679
|
raise typer.Exit()
|
|
678
680
|
|
|
679
681
|
|
|
680
|
-
# ANSI Shadow block wordmark
|
|
681
|
-
#
|
|
682
|
-
|
|
683
|
-
"
|
|
684
|
-
"
|
|
685
|
-
"
|
|
686
|
-
"
|
|
687
|
-
"
|
|
688
|
-
"
|
|
689
|
-
)
|
|
690
|
-
_WELCOME_CODE = (
|
|
691
|
-
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
692
|
-
"██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
693
|
-
"██║ ██║ ██║██║ ██║█████╗ ",
|
|
694
|
-
"██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
695
|
-
"╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
696
|
-
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
682
|
+
# ANSI Shadow block wordmark: large "ASK" with a spaced "ENGINE" label beneath —
|
|
683
|
+
# the product identity (ASK Engine), not the legacy "sourcecode".
|
|
684
|
+
_WELCOME_ASK = (
|
|
685
|
+
" █████╗ ███████╗██╗ ██╗",
|
|
686
|
+
"██╔══██╗██╔════╝██║ ██╔╝",
|
|
687
|
+
"███████║███████╗█████╔╝ ",
|
|
688
|
+
"██╔══██║╚════██║██╔═██╗ ",
|
|
689
|
+
"██║ ██║███████║██║ ██╗",
|
|
690
|
+
"╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝",
|
|
697
691
|
)
|
|
692
|
+
_WELCOME_ENGINE = "E N G I N E"
|
|
698
693
|
|
|
699
694
|
_WELCOME_CMDS = (
|
|
700
|
-
("
|
|
701
|
-
("
|
|
702
|
-
("
|
|
703
|
-
("
|
|
695
|
+
("ask --compact", "repo summary"),
|
|
696
|
+
("ask migrate-check", "upgrade readiness (free)"),
|
|
697
|
+
("ask prepare-context onboard", "onboarding"),
|
|
698
|
+
("ask mcp init", "connect IDE"),
|
|
704
699
|
)
|
|
705
700
|
|
|
706
701
|
|
|
707
702
|
def _print_welcome_plain(tier: str) -> None:
|
|
708
703
|
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
709
|
-
lines = ["", f" ASK Engine {__version__} · {tier} · CLI:
|
|
704
|
+
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
705
|
+
" Actionable Software Knowledge Engine", "",
|
|
710
706
|
" AI coding-agent context, instant.", "", " Get started:"]
|
|
711
707
|
for cmd, desc in _WELCOME_CMDS:
|
|
712
708
|
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
713
709
|
lines.append("")
|
|
714
|
-
lines.append("
|
|
710
|
+
lines.append(" ask --help all commands")
|
|
715
711
|
if tier != "Pro":
|
|
716
|
-
lines.append("
|
|
712
|
+
lines.append(" ask activate <key> unlock Pro")
|
|
717
713
|
lines.append("")
|
|
718
714
|
typer.echo("\n".join(lines))
|
|
719
715
|
|
|
@@ -741,20 +737,21 @@ def _print_welcome() -> None:
|
|
|
741
737
|
|
|
742
738
|
pad = max(len(c) for c, _ in _WELCOME_CMDS) + 2
|
|
743
739
|
tier_style = "green" if tier != "Pro" else "magenta"
|
|
744
|
-
|
|
740
|
+
ask_w = max(len(s) for s in _WELCOME_ASK)
|
|
745
741
|
|
|
746
742
|
t = Text()
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
for i, ln in enumerate(_WELCOME_CODE):
|
|
751
|
-
t.append(ln.ljust(code_w), style="bold cyan")
|
|
743
|
+
# Large "ASK" wordmark; version + tier ride the right of its middle rows.
|
|
744
|
+
for i, ln in enumerate(_WELCOME_ASK):
|
|
745
|
+
t.append(ln.ljust(ask_w), style="bold cyan")
|
|
752
746
|
if i == 2:
|
|
753
747
|
t.append(f" {__version__}", style="dim")
|
|
754
748
|
elif i == 3:
|
|
755
749
|
t.append(" ")
|
|
756
750
|
t.append(tier, style=tier_style)
|
|
757
751
|
t.append("\n")
|
|
752
|
+
# "ENGINE" label beneath the wordmark, with the acronym expansion.
|
|
753
|
+
t.append(_WELCOME_ENGINE, style="bold dim cyan")
|
|
754
|
+
t.append(" Actionable Software Knowledge\n", style="dim")
|
|
758
755
|
|
|
759
756
|
t.append("\nAI coding-agent context, instant.\n\n", style="white")
|
|
760
757
|
|
|
@@ -1016,10 +1013,10 @@ def main(
|
|
|
1016
1013
|
|
|
1017
1014
|
\b
|
|
1018
1015
|
Examples:
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1016
|
+
ask --compact high-signal summary (recommended)
|
|
1017
|
+
ask --compact --git-context include git hotspots
|
|
1018
|
+
ask /path/to/repo --compact analyze specific path
|
|
1019
|
+
ask --agent agent-optimized output (full detail)
|
|
1023
1020
|
"""
|
|
1024
1021
|
# First-run telemetry notice (skip for telemetry/version/config subcommands)
|
|
1025
1022
|
if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
|
|
@@ -2777,14 +2774,14 @@ def prepare_context_cmd(
|
|
|
2777
2774
|
|
|
2778
2775
|
\b
|
|
2779
2776
|
Examples:
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2777
|
+
ask prepare-context explain
|
|
2778
|
+
ask prepare-context explain /path/to/repo
|
|
2779
|
+
ask prepare-context fix-bug
|
|
2780
|
+
ask prepare-context delta --since main
|
|
2781
|
+
ask prepare-context review-pr --since origin/main
|
|
2782
|
+
ask prepare-context review-pr . --since main --output review.json
|
|
2783
|
+
ask prepare-context onboard --llm-prompt
|
|
2784
|
+
ask prepare-context --task-help
|
|
2788
2785
|
"""
|
|
2789
2786
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
2790
2787
|
|
|
@@ -2835,7 +2832,7 @@ def prepare_context_cmd(
|
|
|
2835
2832
|
"free_tier_note": (
|
|
2836
2833
|
f"Free quota of {30} delta runs per repository exhausted."
|
|
2837
2834
|
),
|
|
2838
|
-
"free_tier_alternative": "
|
|
2835
|
+
"free_tier_alternative": "ask prepare-context review-pr --since <ref>",
|
|
2839
2836
|
},
|
|
2840
2837
|
)
|
|
2841
2838
|
# Within quota: emit a header note so CI logs show remaining runs.
|
|
@@ -3347,7 +3344,7 @@ def telemetry_status() -> None:
|
|
|
3347
3344
|
if not asked:
|
|
3348
3345
|
typer.echo(" (first-run notice not yet shown — will show on next run)")
|
|
3349
3346
|
typer.echo(f" Config: {config_file_path()}")
|
|
3350
|
-
typer.echo(" Disable:
|
|
3347
|
+
typer.echo(" Disable: ask telemetry disable")
|
|
3351
3348
|
typer.echo(" Or set env var: SOURCECODE_TELEMETRY=0 (or DO_NOT_TRACK=1)")
|
|
3352
3349
|
|
|
3353
3350
|
|
|
@@ -3360,7 +3357,7 @@ def telemetry_enable() -> None:
|
|
|
3360
3357
|
typer.echo("Telemetry enabled. Thank you — this helps improve sourcecode.")
|
|
3361
3358
|
typer.echo("What is collected: version, OS, commands, flags, duration, repo size range, errors.")
|
|
3362
3359
|
typer.echo("What is never collected: source code, paths, secrets, or any output content.")
|
|
3363
|
-
typer.echo("Disable at any time:
|
|
3360
|
+
typer.echo("Disable at any time: ask telemetry disable")
|
|
3364
3361
|
_tel.record("telemetry_enabled", cmd="telemetry")
|
|
3365
3362
|
|
|
3366
3363
|
|
|
@@ -3371,7 +3368,7 @@ def telemetry_disable() -> None:
|
|
|
3371
3368
|
set_enabled(False)
|
|
3372
3369
|
typer.echo("Telemetry disabled. No data will be collected or sent.")
|
|
3373
3370
|
typer.echo("Telemetry is on by default; this opt-out is remembered.")
|
|
3374
|
-
typer.echo("Re-enable at any time:
|
|
3371
|
+
typer.echo("Re-enable at any time: ask telemetry enable")
|
|
3375
3372
|
|
|
3376
3373
|
|
|
3377
3374
|
def _serialize_dict(data: dict, format: str) -> str:
|
|
@@ -3474,13 +3471,13 @@ def repo_ir_cmd(
|
|
|
3474
3471
|
|
|
3475
3472
|
\b
|
|
3476
3473
|
Examples:
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3474
|
+
ask repo-ir
|
|
3475
|
+
ask repo-ir /path/to/repo --since HEAD~1
|
|
3476
|
+
ask repo-ir --files src/main/java/UserService.java
|
|
3477
|
+
ask repo-ir --since main --output ir.json
|
|
3478
|
+
ask repo-ir --since HEAD~3 --summary-only --output ir-small.json
|
|
3479
|
+
ask repo-ir --max-nodes 200 --max-edges 500
|
|
3480
|
+
ask repo-ir --output ir.json.gz --gzip
|
|
3484
3481
|
"""
|
|
3485
3482
|
import json as _json
|
|
3486
3483
|
|
|
@@ -3705,13 +3702,13 @@ def impact_cmd(
|
|
|
3705
3702
|
|
|
3706
3703
|
\b
|
|
3707
3704
|
NOTE: Free on repos up to the size limit; Pro unlocks enterprise-scale
|
|
3708
|
-
monoliths. Run '
|
|
3705
|
+
monoliths. Run 'ask license' for details.
|
|
3709
3706
|
|
|
3710
3707
|
\b
|
|
3711
3708
|
Examples:
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3709
|
+
ask impact UserService
|
|
3710
|
+
ask impact org.keycloak.services.DefaultKeycloakSession /path/to/keycloak
|
|
3711
|
+
ask impact UserService --depth 6 --output impact.json
|
|
3715
3712
|
"""
|
|
3716
3713
|
from sourcecode.license import require_repo_or_pro as _require_repo_or_pro
|
|
3717
3714
|
_require_repo_or_pro(str(path.resolve()), "impact")
|
|
@@ -3733,7 +3730,7 @@ def impact_cmd(
|
|
|
3733
3730
|
sys.stderr.write(
|
|
3734
3731
|
f"[impact] Legacy argument order detected: '{target}' is a directory, not a class name.\n"
|
|
3735
3732
|
f"[impact] Swapping: target='{path}', path='{target}'. "
|
|
3736
|
-
f"New syntax:
|
|
3733
|
+
f"New syntax: ask impact <target> [path]\n"
|
|
3737
3734
|
)
|
|
3738
3735
|
sys.stderr.flush()
|
|
3739
3736
|
target, path = str(path), _target_as_path
|
|
@@ -3742,7 +3739,7 @@ def impact_cmd(
|
|
|
3742
3739
|
_emit_error_json(
|
|
3743
3740
|
INVALID_INPUT_CODE,
|
|
3744
3741
|
"Class name must not be empty.",
|
|
3745
|
-
hint="Pass a class name or FQN. Example:
|
|
3742
|
+
hint="Pass a class name or FQN. Example: ask impact OrderService .",
|
|
3746
3743
|
expected="A non-empty class name or FQN.",
|
|
3747
3744
|
)
|
|
3748
3745
|
raise typer.Exit(1)
|
|
@@ -3755,7 +3752,7 @@ def impact_cmd(
|
|
|
3755
3752
|
path=str(root),
|
|
3756
3753
|
hint=(
|
|
3757
3754
|
"Pass an existing repository directory as the second argument. "
|
|
3758
|
-
"New syntax:
|
|
3755
|
+
"New syntax: ask impact <target> [path] — "
|
|
3759
3756
|
"target is the class name, path is the repo root."
|
|
3760
3757
|
),
|
|
3761
3758
|
expected="A directory path.",
|
|
@@ -3896,13 +3893,13 @@ def endpoints_cmd(
|
|
|
3896
3893
|
|
|
3897
3894
|
\b
|
|
3898
3895
|
Examples:
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3896
|
+
ask endpoints .
|
|
3897
|
+
ask endpoints /path/to/repo
|
|
3898
|
+
ask endpoints . --output endpoints.json
|
|
3899
|
+
ask endpoints . --format yaml
|
|
3900
|
+
ask endpoints . --path-prefix /v1/liquidacion
|
|
3901
|
+
ask endpoints . --controller LiquidacionJornada
|
|
3902
|
+
ask endpoints . --limit 10
|
|
3906
3903
|
"""
|
|
3907
3904
|
_enforce_format("endpoints", format)
|
|
3908
3905
|
|
|
@@ -3917,7 +3914,12 @@ def endpoints_cmd(
|
|
|
3917
3914
|
)
|
|
3918
3915
|
raise typer.Exit(code=1)
|
|
3919
3916
|
|
|
3920
|
-
|
|
3917
|
+
_prog = Progress()
|
|
3918
|
+
_prog.start("scanning endpoints")
|
|
3919
|
+
try:
|
|
3920
|
+
data = _extract_java_endpoints(target)
|
|
3921
|
+
finally:
|
|
3922
|
+
_prog.finish()
|
|
3921
3923
|
|
|
3922
3924
|
# Update RIS api_surface section (non-fatal side-effect).
|
|
3923
3925
|
try:
|
|
@@ -4364,6 +4366,9 @@ def export_cmd(
|
|
|
4364
4366
|
)
|
|
4365
4367
|
raise typer.Exit(1)
|
|
4366
4368
|
|
|
4369
|
+
_prog = Progress()
|
|
4370
|
+
_prog.start("scanning repository")
|
|
4371
|
+
|
|
4367
4372
|
file_list = [
|
|
4368
4373
|
f for f in find_java_files(root)
|
|
4369
4374
|
if "/test/" not in f and "/tests/" not in f
|
|
@@ -4390,6 +4395,7 @@ def export_cmd(
|
|
|
4390
4395
|
_integrations,
|
|
4391
4396
|
endpoint_meta=_ep_data,
|
|
4392
4397
|
)
|
|
4398
|
+
_prog.finish()
|
|
4393
4399
|
output = _serialize_dict(data, format)
|
|
4394
4400
|
_emit_command_output(output, output_path, copy,
|
|
4395
4401
|
success_msg=f"C4 architecture export written to {output_path}")
|
|
@@ -4419,6 +4425,7 @@ def export_cmd(
|
|
|
4419
4425
|
ContextGraph.build(file_list, root)
|
|
4420
4426
|
).as_report(len(file_list))
|
|
4421
4427
|
|
|
4428
|
+
_prog.finish()
|
|
4422
4429
|
output = _serialize_dict(data, format)
|
|
4423
4430
|
_emit_command_output(output, output_path, copy,
|
|
4424
4431
|
success_msg=f"Export written to {output_path}")
|
|
@@ -4481,10 +4488,10 @@ def validation_cmd(
|
|
|
4481
4488
|
|
|
4482
4489
|
\b
|
|
4483
4490
|
Examples:
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4491
|
+
ask validation .
|
|
4492
|
+
ask validation . --gaps-only
|
|
4493
|
+
ask validation . --path-prefix /owners
|
|
4494
|
+
ask validation . --format yaml
|
|
4488
4495
|
"""
|
|
4489
4496
|
_enforce_format("validation", format)
|
|
4490
4497
|
|
|
@@ -4501,8 +4508,21 @@ def validation_cmd(
|
|
|
4501
4508
|
|
|
4502
4509
|
from sourcecode.context_graph import ContextGraph
|
|
4503
4510
|
from sourcecode.validation_surface import build_validation_surface
|
|
4511
|
+
_prog = Progress()
|
|
4512
|
+
_prog.start("mapping validation surface")
|
|
4504
4513
|
# Structural facts come from the ContextGraph — the single access layer.
|
|
4505
|
-
|
|
4514
|
+
_graph = ContextGraph.build_from_root(target)
|
|
4515
|
+
data = build_validation_surface(target, graph=_graph)
|
|
4516
|
+
|
|
4517
|
+
# P1-C: classify the request-body validation *pattern* so a repo with a
|
|
4518
|
+
# validation framework on the classpath but no @Valid reads as
|
|
4519
|
+
# "present-but-unused" instead of a silent sea of zeros (DESIGN §2/§5).
|
|
4520
|
+
try:
|
|
4521
|
+
from sourcecode.validation_inference import infer_validation_pattern
|
|
4522
|
+
|
|
4523
|
+
data["validation_pattern"] = infer_validation_pattern(_graph.cir).to_dict()
|
|
4524
|
+
except Exception:
|
|
4525
|
+
pass
|
|
4506
4526
|
|
|
4507
4527
|
if path_prefix:
|
|
4508
4528
|
data["endpoints"] = [
|
|
@@ -4522,6 +4542,7 @@ def validation_cmd(
|
|
|
4522
4542
|
if _note:
|
|
4523
4543
|
data["note"] = _note
|
|
4524
4544
|
|
|
4545
|
+
_prog.finish()
|
|
4525
4546
|
output = _serialize_dict(data, format)
|
|
4526
4547
|
_summary = data.get("summary", {})
|
|
4527
4548
|
# Human-facing heads-up when the result is empty purely because no OpenAPI
|
|
@@ -4676,17 +4697,17 @@ def spring_audit_cmd(
|
|
|
4676
4697
|
|
|
4677
4698
|
\b
|
|
4678
4699
|
CI/CD usage:
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4700
|
+
ask spring-audit . --ci # exit 1 on any finding
|
|
4701
|
+
ask spring-audit . --ci --min-severity high # exit 1 only on high/critical
|
|
4702
|
+
ask spring-audit . --ci --format github-comment # Markdown PR comment + exit 1
|
|
4682
4703
|
|
|
4683
4704
|
\b
|
|
4684
4705
|
Examples:
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4706
|
+
ask spring-audit .
|
|
4707
|
+
ask spring-audit /path/to/repo
|
|
4708
|
+
ask spring-audit . --scope security
|
|
4709
|
+
ask spring-audit . --min-severity high
|
|
4710
|
+
ask spring-audit . --output audit.json
|
|
4690
4711
|
"""
|
|
4691
4712
|
import json as _json
|
|
4692
4713
|
|
|
@@ -4745,6 +4766,8 @@ def spring_audit_cmd(
|
|
|
4745
4766
|
success_msg=f"Spring audit written to {output_path}")
|
|
4746
4767
|
return
|
|
4747
4768
|
|
|
4769
|
+
_prog = Progress()
|
|
4770
|
+
_prog.start(f"auditing {len(file_list)} Java files")
|
|
4748
4771
|
cir = ContextGraph.build(file_list, target).cir
|
|
4749
4772
|
_model = SpringSemanticModel.build(cir)
|
|
4750
4773
|
|
|
@@ -4797,6 +4820,7 @@ def spring_audit_cmd(
|
|
|
4797
4820
|
except Exception:
|
|
4798
4821
|
pass
|
|
4799
4822
|
|
|
4823
|
+
_prog.finish()
|
|
4800
4824
|
if format == "github-comment":
|
|
4801
4825
|
output = _render_spring_audit_github_comment(combined, min_severity)
|
|
4802
4826
|
else:
|
|
@@ -4882,11 +4906,11 @@ def migrate_check_cmd(
|
|
|
4882
4906
|
|
|
4883
4907
|
\b
|
|
4884
4908
|
Examples:
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4909
|
+
ask migrate-check .
|
|
4910
|
+
ask migrate-check . --compact bounded decision summary
|
|
4911
|
+
ask migrate-check /path/to/repo --format text
|
|
4912
|
+
ask migrate-check . --min-severity high
|
|
4913
|
+
ask migrate-check . --output migration.json
|
|
4890
4914
|
"""
|
|
4891
4915
|
from sourcecode.repository_ir import find_java_files
|
|
4892
4916
|
from sourcecode.migrate_check import run_migrate_check
|
|
@@ -4915,7 +4939,12 @@ def migrate_check_cmd(
|
|
|
4915
4939
|
|
|
4916
4940
|
_file_limitations: list[str] = []
|
|
4917
4941
|
file_list = find_java_files(target, limitations=_file_limitations)
|
|
4918
|
-
|
|
4942
|
+
_prog = Progress()
|
|
4943
|
+
_prog.start(f"checking migration ({len(file_list)} files)")
|
|
4944
|
+
try:
|
|
4945
|
+
report = run_migrate_check(file_list, target, min_severity=min_severity)
|
|
4946
|
+
finally:
|
|
4947
|
+
_prog.finish()
|
|
4919
4948
|
if _file_limitations:
|
|
4920
4949
|
report.limitations.extend(_file_limitations)
|
|
4921
4950
|
|
|
@@ -5011,9 +5040,9 @@ def impact_chain_cmd(
|
|
|
5011
5040
|
|
|
5012
5041
|
\b
|
|
5013
5042
|
Examples:
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5043
|
+
ask impact-chain OrderService .
|
|
5044
|
+
ask impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
5045
|
+
ask impact-chain PaymentService . --depth 6 --output impact.json
|
|
5017
5046
|
"""
|
|
5018
5047
|
import json as _json
|
|
5019
5048
|
|
|
@@ -5027,7 +5056,7 @@ def impact_chain_cmd(
|
|
|
5027
5056
|
_emit_error_json(
|
|
5028
5057
|
INVALID_INPUT_CODE,
|
|
5029
5058
|
"Symbol name must not be empty.",
|
|
5030
|
-
hint="Pass a class name or FQN. Example:
|
|
5059
|
+
hint="Pass a class name or FQN. Example: ask impact-chain OrderService .",
|
|
5031
5060
|
expected="A non-empty class name or FQN.",
|
|
5032
5061
|
)
|
|
5033
5062
|
raise typer.Exit(code=1)
|
|
@@ -5074,6 +5103,8 @@ def impact_chain_cmd(
|
|
|
5074
5103
|
success_msg=f"Impact chain written to {output_path}")
|
|
5075
5104
|
return
|
|
5076
5105
|
|
|
5106
|
+
_prog = Progress()
|
|
5107
|
+
_prog.start(f"analyzing impact ({len(file_list)} files)")
|
|
5077
5108
|
cir = ContextGraph.build(file_list, target).cir
|
|
5078
5109
|
_model = SpringSemanticModel.build(cir)
|
|
5079
5110
|
|
|
@@ -5081,6 +5112,7 @@ def impact_chain_cmd(
|
|
|
5081
5112
|
from sourcecode.spring_event_topology import run_event_topology
|
|
5082
5113
|
evt_result = run_event_topology(cir, symbol, model=_model)
|
|
5083
5114
|
data = evt_result.to_dict()
|
|
5115
|
+
_prog.finish()
|
|
5084
5116
|
output = _serialize_dict(data, format)
|
|
5085
5117
|
_emit_command_output(
|
|
5086
5118
|
output, output_path, copy,
|
|
@@ -5096,6 +5128,7 @@ def impact_chain_cmd(
|
|
|
5096
5128
|
result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
|
|
5097
5129
|
|
|
5098
5130
|
data = result.to_dict()
|
|
5131
|
+
_prog.finish()
|
|
5099
5132
|
output = _serialize_dict(data, format)
|
|
5100
5133
|
_emit_command_output(
|
|
5101
5134
|
output, output_path, copy,
|
|
@@ -5155,9 +5188,9 @@ def pr_impact_cmd(
|
|
|
5155
5188
|
|
|
5156
5189
|
\b
|
|
5157
5190
|
Examples:
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5191
|
+
ask pr-impact --files changed_files.txt
|
|
5192
|
+
ask pr-impact /path/to/repo --files diff.txt --format json
|
|
5193
|
+
ask pr-impact --files changes.txt --output pr_report.txt
|
|
5161
5194
|
"""
|
|
5162
5195
|
import json as _json
|
|
5163
5196
|
|
|
@@ -5184,7 +5217,7 @@ def pr_impact_cmd(
|
|
|
5184
5217
|
path=str(files),
|
|
5185
5218
|
hint=(
|
|
5186
5219
|
"Create a file with one changed Java file path per line, then pass it with --files. "
|
|
5187
|
-
"Example: git diff --name-only HEAD~1 > changed.txt &&
|
|
5220
|
+
"Example: git diff --name-only HEAD~1 > changed.txt && ask pr-impact . --files changed.txt"
|
|
5188
5221
|
),
|
|
5189
5222
|
expected="A text file containing one Java file path per line.",
|
|
5190
5223
|
)
|
|
@@ -5224,9 +5257,14 @@ def pr_impact_cmd(
|
|
|
5224
5257
|
success_msg=f"PR impact report written to {output_path}")
|
|
5225
5258
|
return
|
|
5226
5259
|
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5260
|
+
_prog = Progress()
|
|
5261
|
+
_prog.start(f"analyzing PR impact ({len(file_list)} files)")
|
|
5262
|
+
try:
|
|
5263
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5264
|
+
model = SpringSemanticModel.build(cir)
|
|
5265
|
+
report = run_pr_impact(cir, changed_files, root=target, model=model)
|
|
5266
|
+
finally:
|
|
5267
|
+
_prog.finish()
|
|
5230
5268
|
|
|
5231
5269
|
output = _serialize_dict(report.to_dict(), "json") if format == "json" else report.render_text()
|
|
5232
5270
|
_emit_command_output(
|
|
@@ -5291,9 +5329,9 @@ def explain_cmd(
|
|
|
5291
5329
|
|
|
5292
5330
|
\b
|
|
5293
5331
|
Examples:
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5332
|
+
ask explain UserService
|
|
5333
|
+
ask explain OrderController /path/to/repo
|
|
5334
|
+
ask explain UserService --format json
|
|
5297
5335
|
"""
|
|
5298
5336
|
import json as _json
|
|
5299
5337
|
|
|
@@ -5306,7 +5344,7 @@ def explain_cmd(
|
|
|
5306
5344
|
_emit_error_json(
|
|
5307
5345
|
INVALID_INPUT_CODE,
|
|
5308
5346
|
"Class name must not be empty.",
|
|
5309
|
-
hint="Pass a class name. Example:
|
|
5347
|
+
hint="Pass a class name. Example: ask explain UserService .",
|
|
5310
5348
|
expected="A non-empty class name.",
|
|
5311
5349
|
)
|
|
5312
5350
|
raise typer.Exit(code=1)
|
|
@@ -5338,14 +5376,19 @@ def explain_cmd(
|
|
|
5338
5376
|
_emit_error_json(
|
|
5339
5377
|
INVALID_INPUT_CODE,
|
|
5340
5378
|
f"No Java files found in '{target}'.",
|
|
5341
|
-
hint="
|
|
5379
|
+
hint="ask explain requires a Java/Spring repository.",
|
|
5342
5380
|
expected="A directory containing .java source files.",
|
|
5343
5381
|
)
|
|
5344
5382
|
raise typer.Exit(code=1)
|
|
5345
5383
|
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5384
|
+
_prog = Progress()
|
|
5385
|
+
_prog.start(f"explaining {class_name} ({len(file_list)} files)")
|
|
5386
|
+
try:
|
|
5387
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5388
|
+
model = SpringSemanticModel.build(cir)
|
|
5389
|
+
explanation = explain_class(class_name, cir, model)
|
|
5390
|
+
finally:
|
|
5391
|
+
_prog.finish()
|
|
5349
5392
|
|
|
5350
5393
|
if format == "json":
|
|
5351
5394
|
output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
|
|
@@ -5399,16 +5442,16 @@ def onboard_cmd(
|
|
|
5399
5442
|
|
|
5400
5443
|
\b
|
|
5401
5444
|
Workflow:
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5445
|
+
ask onboard .
|
|
5446
|
+
ask onboard /path/to/repo --llm-prompt
|
|
5447
|
+
ask onboard . --output onboard.json
|
|
5405
5448
|
|
|
5406
5449
|
\b
|
|
5407
5450
|
Related workflows:
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5451
|
+
ask impact <target> — What breaks if I touch this?
|
|
5452
|
+
ask review-pr — Risk-rank a pending PR
|
|
5453
|
+
ask modernize . — Where should I refactor first?
|
|
5454
|
+
ask fix-bug — Where does this symptom live?
|
|
5412
5455
|
"""
|
|
5413
5456
|
ctx.invoke(
|
|
5414
5457
|
prepare_context_cmd,
|
|
@@ -5468,14 +5511,14 @@ def review_pr_cmd(
|
|
|
5468
5511
|
|
|
5469
5512
|
\b
|
|
5470
5513
|
Workflow:
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5514
|
+
ask review-pr --since origin/main
|
|
5515
|
+
ask review-pr . --since main --format github-comment
|
|
5516
|
+
ask review-pr . --since HEAD~3 --output review.json
|
|
5474
5517
|
|
|
5475
5518
|
\b
|
|
5476
5519
|
Related workflows:
|
|
5477
|
-
|
|
5478
|
-
|
|
5520
|
+
ask impact <target> — Single-symbol blast radius
|
|
5521
|
+
ask onboard . — Full architecture onboarding
|
|
5479
5522
|
"""
|
|
5480
5523
|
ctx.invoke(
|
|
5481
5524
|
prepare_context_cmd,
|
|
@@ -5528,14 +5571,14 @@ def fix_bug_cmd(
|
|
|
5528
5571
|
|
|
5529
5572
|
\b
|
|
5530
5573
|
Workflow:
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5574
|
+
ask fix-bug --symptom "NullPointerException in UserService"
|
|
5575
|
+
ask fix-bug . --symptom "401 on /api/orders"
|
|
5576
|
+
ask fix-bug . --output bug-context.json
|
|
5534
5577
|
|
|
5535
5578
|
\b
|
|
5536
5579
|
Related workflows:
|
|
5537
|
-
|
|
5538
|
-
|
|
5580
|
+
ask impact <target> — Propagate impact from a specific class
|
|
5581
|
+
ask onboard . — Full architecture context first
|
|
5539
5582
|
"""
|
|
5540
5583
|
# Detect misuse: `fix-bug "symptom text" /path` — path arg looks like a symptom.
|
|
5541
5584
|
_path_str = str(path)
|
|
@@ -5547,7 +5590,7 @@ def fix_bug_cmd(
|
|
|
5547
5590
|
_emit_error_json(
|
|
5548
5591
|
INVALID_INPUT_CODE,
|
|
5549
5592
|
f"'{_path_str}' is not a valid directory. Did you mean to use --symptom?",
|
|
5550
|
-
hint=f"Use:
|
|
5593
|
+
hint=f"Use: ask fix-bug . --symptom {_path_str!r}",
|
|
5551
5594
|
expected="A repository directory path as first argument.",
|
|
5552
5595
|
)
|
|
5553
5596
|
raise typer.Exit(code=1)
|
|
@@ -5658,7 +5701,56 @@ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[
|
|
|
5658
5701
|
if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
|
|
5659
5702
|
break
|
|
5660
5703
|
|
|
5661
|
-
|
|
5704
|
+
# 3. Nested-type qualified-reference scan (SC-2 residual). The static call-graph
|
|
5705
|
+
# does not emit an edge for a nested-type member access in ordinary method-body
|
|
5706
|
+
# code — e.g. `PropertyType.AdminGroupPresentation.NAME` credits the enclosing
|
|
5707
|
+
# type, not the nested holder — so a referenced nested class can look
|
|
5708
|
+
# zero-degree. When a candidate is a nested type (its second-to-last FQN segment
|
|
5709
|
+
# is an UpperCamel enclosing type) and its qualified `Outer.Nested` form appears
|
|
5710
|
+
# in some OTHER Java source, it is statically referenced: neither dead nor
|
|
5711
|
+
# framework-dispatched. Structural, name-agnostic (pattern derived from the
|
|
5712
|
+
# candidate's own FQN).
|
|
5713
|
+
import re as _re_nested
|
|
5714
|
+
statically_referenced: set[str] = set()
|
|
5715
|
+
nested_patterns: dict[str, "tuple"] = {}
|
|
5716
|
+
for n in nodes:
|
|
5717
|
+
if n["fqn"] in dispatched_fqns:
|
|
5718
|
+
continue
|
|
5719
|
+
parts = (n.get("fqn") or "").split(".")
|
|
5720
|
+
if len(parts) >= 2 and parts[-2][:1].isupper():
|
|
5721
|
+
outer, nested = parts[-2], parts[-1]
|
|
5722
|
+
nested_patterns[n["fqn"]] = (
|
|
5723
|
+
_re_nested.compile(r"\b" + _re_nested.escape(outer) + r"\." + _re_nested.escape(nested) + r"\b"),
|
|
5724
|
+
n.get("source_file") or "",
|
|
5725
|
+
)
|
|
5726
|
+
if nested_patterns:
|
|
5727
|
+
files_scanned = 0
|
|
5728
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
5729
|
+
dirnames[:] = [d for d in dirnames
|
|
5730
|
+
if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
|
|
5731
|
+
for fname in filenames:
|
|
5732
|
+
if not fname.endswith(".java"):
|
|
5733
|
+
continue
|
|
5734
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5735
|
+
break
|
|
5736
|
+
fpath = os.path.join(dirpath, fname)
|
|
5737
|
+
rel = os.path.relpath(fpath, root)
|
|
5738
|
+
try:
|
|
5739
|
+
with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
|
|
5740
|
+
text = fh.read(_CONFIG_SCAN_MAX_BYTES)
|
|
5741
|
+
except OSError:
|
|
5742
|
+
continue
|
|
5743
|
+
files_scanned += 1
|
|
5744
|
+
for fqn, (pat, own) in nested_patterns.items():
|
|
5745
|
+
if fqn in statically_referenced or rel == own:
|
|
5746
|
+
continue
|
|
5747
|
+
if pat.search(text):
|
|
5748
|
+
statically_referenced.add(fqn)
|
|
5749
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5750
|
+
break
|
|
5751
|
+
|
|
5752
|
+
unreferenced = [n for n in nodes
|
|
5753
|
+
if n["fqn"] not in dispatched_fqns and n["fqn"] not in statically_referenced]
|
|
5662
5754
|
dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
|
|
5663
5755
|
return unreferenced, dispatched
|
|
5664
5756
|
|
|
@@ -5697,13 +5789,13 @@ def modernize_cmd(
|
|
|
5697
5789
|
|
|
5698
5790
|
\b
|
|
5699
5791
|
Workflow:
|
|
5700
|
-
|
|
5701
|
-
|
|
5792
|
+
ask modernize .
|
|
5793
|
+
ask modernize /path/to/repo --output modernize.json
|
|
5702
5794
|
|
|
5703
5795
|
\b
|
|
5704
5796
|
Related workflows:
|
|
5705
|
-
|
|
5706
|
-
|
|
5797
|
+
ask onboard . — Architecture overview first
|
|
5798
|
+
ask impact <target> — Verify impact before touching a hotspot
|
|
5707
5799
|
"""
|
|
5708
5800
|
import json as _json
|
|
5709
5801
|
from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
|
|
@@ -5865,8 +5957,13 @@ def modernize_cmd(
|
|
|
5865
5957
|
"edge_count": _cnt,
|
|
5866
5958
|
"reverse_edge_count": _reverse,
|
|
5867
5959
|
# A mutual (cyclic) dependency is the actual "tangle" — both modules
|
|
5868
|
-
# reference each other, so neither can be extracted independently.
|
|
5960
|
+
# reference each other, so neither can be extracted independently. A
|
|
5961
|
+
# one-way (directional) dependency is normal layering, NOT a tangle:
|
|
5962
|
+
# TANGLE-1 (petclinic field test) surfaced owner→model / vet→model
|
|
5963
|
+
# (reverse_edge_count:0) being labelled "tangles" when they are plain
|
|
5964
|
+
# base-model usage. coupling_type states which it is, honestly.
|
|
5869
5965
|
"mutual": _reverse > 0,
|
|
5966
|
+
"coupling_type": "cyclic" if _reverse > 0 else "directional",
|
|
5870
5967
|
"example": _pair_example.get((_a, _b), ""),
|
|
5871
5968
|
})
|
|
5872
5969
|
# Surface mutual tangles first (highest decomposition risk), then by strength.
|
|
@@ -5942,17 +6039,10 @@ def modernize_cmd(
|
|
|
5942
6039
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5943
6040
|
for n in coupling_nodes
|
|
5944
6041
|
],
|
|
5945
|
-
#
|
|
5946
|
-
# so it is not confused with `explain`'s caller
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
"(imports, injects, extends/implements, references, annotations), "
|
|
5950
|
-
"counted at symbol level. This is deliberately larger than and NOT "
|
|
5951
|
-
"equal to `sourcecode explain`'s caller count, which reports DISTINCT "
|
|
5952
|
-
"dependent classes (DI dependents + reverse-call-graph callers, "
|
|
5953
|
-
"deduplicated to class level). Use in_degree for blast-radius ranking, "
|
|
5954
|
-
"explain's caller list for the concrete dependents to inspect."
|
|
5955
|
-
),
|
|
6042
|
+
# SC-6 (Broadleaf field test): make the in_degree metric self-describing
|
|
6043
|
+
# so it is not confused with `explain`'s / `impact`'s caller counts. All
|
|
6044
|
+
# three share one reconciliation note so they can never contradict.
|
|
6045
|
+
"high_coupling_nodes_note": CALLER_METRIC_RECONCILIATION,
|
|
5956
6046
|
"statically_unreferenced": [
|
|
5957
6047
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5958
6048
|
for n in dead_zones
|
|
@@ -5973,10 +6063,12 @@ def modernize_cmd(
|
|
|
5973
6063
|
"cross_module_tangles_note": (
|
|
5974
6064
|
"Directed inter-subsystem coupling measured from structural graph "
|
|
5975
6065
|
"edges (imports/injects/extends/implements/references/calls). "
|
|
5976
|
-
"edge_count = edges from_package→to_package
|
|
5977
|
-
"
|
|
5978
|
-
"independent module extraction
|
|
5979
|
-
"
|
|
6066
|
+
"edge_count = edges from_package→to_package. coupling_type=cyclic "
|
|
6067
|
+
"(mutual=true) is a bidirectional dependency — the actual tangle "
|
|
6068
|
+
"that blocks independent module extraction; coupling_type="
|
|
6069
|
+
"directional (reverse_edge_count=0) is a normal one-way layered "
|
|
6070
|
+
"dependency, listed for coupling strength, NOT a tangle. Empty "
|
|
6071
|
+
"means no cross-subsystem structural coupling was detected."
|
|
5980
6072
|
),
|
|
5981
6073
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
5982
6074
|
"recommendation": (
|
|
@@ -5987,7 +6079,9 @@ def modernize_cmd(
|
|
|
5987
6079
|
)
|
|
5988
6080
|
+ "statically_unreferenced lists classes with no Java callers — review "
|
|
5989
6081
|
+ "for framework dispatch (XML/reflection/SPI) before removing. "
|
|
5990
|
-
+ "
|
|
6082
|
+
+ "In cross_module_tangles, coupling_type=cyclic entries are the "
|
|
6083
|
+
+ "real tangles to decompose first; directional entries are normal "
|
|
6084
|
+
+ "layering ranked by coupling strength."
|
|
5991
6085
|
),
|
|
5992
6086
|
}
|
|
5993
6087
|
|
|
@@ -6061,10 +6155,10 @@ def rename_class_cmd(
|
|
|
6061
6155
|
|
|
6062
6156
|
\b
|
|
6063
6157
|
Examples:
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6158
|
+
ask rename-class . --from ServiceA --to ServiceB
|
|
6159
|
+
ask rename-class /path/to/repo --from OrderManager --to OrderService
|
|
6160
|
+
ask rename-class . --from OldName --to NewName --dry-run
|
|
6161
|
+
ask rename-class . --from OldName --to NewName --output rename-audit.json
|
|
6068
6162
|
"""
|
|
6069
6163
|
import json as _json
|
|
6070
6164
|
from sourcecode.rename_refactor import rename_class
|
|
@@ -6167,10 +6261,10 @@ def chunk_file_cmd(
|
|
|
6167
6261
|
|
|
6168
6262
|
\b
|
|
6169
6263
|
Examples:
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6264
|
+
ask chunk-file NominasCalculoService.java
|
|
6265
|
+
ask chunk-file BigService.java --max-lines 300
|
|
6266
|
+
ask chunk-file BigService.java --chunk 5 # read chunk 5 only
|
|
6267
|
+
ask chunk-file BigService.java --metadata-only # sizes/boundaries only
|
|
6174
6268
|
"""
|
|
6175
6269
|
import json as _json
|
|
6176
6270
|
from sourcecode.file_chunker import chunk_java_file
|
|
@@ -6234,7 +6328,7 @@ def activate_cmd(
|
|
|
6234
6328
|
|
|
6235
6329
|
\b
|
|
6236
6330
|
Examples:
|
|
6237
|
-
|
|
6331
|
+
ask activate SC-XXXX-XXXX-XXXX
|
|
6238
6332
|
"""
|
|
6239
6333
|
from sourcecode.license import activate_license as _activate
|
|
6240
6334
|
_activate(license_key)
|
|
@@ -6300,7 +6394,7 @@ def version_cmd() -> None:
|
|
|
6300
6394
|
"compatibility_schema_version": "1.0"}
|
|
6301
6395
|
"""
|
|
6302
6396
|
if getattr(sys.stdout, "isatty", lambda: False)():
|
|
6303
|
-
typer.echo(f"
|
|
6397
|
+
typer.echo(f"ask {__version__}")
|
|
6304
6398
|
else:
|
|
6305
6399
|
import json as _json_ver
|
|
6306
6400
|
typer.echo(_json_ver.dumps({
|
|
@@ -6316,14 +6410,14 @@ def version_cmd() -> None:
|
|
|
6316
6410
|
def config_cmd() -> None:
|
|
6317
6411
|
"""Show current configuration."""
|
|
6318
6412
|
from sourcecode.telemetry.config import config_file_path, is_enabled
|
|
6319
|
-
typer.echo(f"
|
|
6413
|
+
typer.echo(f"ask {__version__}")
|
|
6320
6414
|
typer.echo(f"Config: {config_file_path()}")
|
|
6321
6415
|
typer.echo(f"Telemetry: {'enabled' if is_enabled() else 'disabled'}")
|
|
6322
6416
|
typer.echo("")
|
|
6323
6417
|
typer.echo("Manage telemetry:")
|
|
6324
|
-
typer.echo("
|
|
6325
|
-
typer.echo("
|
|
6326
|
-
typer.echo("
|
|
6418
|
+
typer.echo(" ask telemetry enable")
|
|
6419
|
+
typer.echo(" ask telemetry disable")
|
|
6420
|
+
typer.echo(" ask telemetry status")
|
|
6327
6421
|
|
|
6328
6422
|
|
|
6329
6423
|
# ── cold-start (RIS bootstrap for external MCP and agents) ───────────────────
|
|
@@ -6367,7 +6461,7 @@ def cold_start_cmd(
|
|
|
6367
6461
|
if isinstance(result.get("endpoints"), list):
|
|
6368
6462
|
result["endpoints"] = result["endpoints"][:30]
|
|
6369
6463
|
result["_meta"] = {**(result.get("_meta") or {}), "compact_mode": True,
|
|
6370
|
-
"full_available": "
|
|
6464
|
+
"full_available": "ask cold-start (without --compact)"}
|
|
6371
6465
|
_out = _json.dumps(result, indent=2, ensure_ascii=False)
|
|
6372
6466
|
_size = len(_out.encode("utf-8"))
|
|
6373
6467
|
_tokens = _size // 4
|
|
@@ -6458,13 +6552,13 @@ def mcp_init(
|
|
|
6458
6552
|
|
|
6459
6553
|
\b
|
|
6460
6554
|
Detects installed MCP clients, backs up their config files, and safely
|
|
6461
|
-
inserts the
|
|
6555
|
+
inserts the ASK Engine server entry. Fully idempotent — safe to re-run.
|
|
6462
6556
|
|
|
6463
6557
|
\b
|
|
6464
6558
|
Examples:
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6559
|
+
ask mcp init
|
|
6560
|
+
ask mcp init --target claude-desktop
|
|
6561
|
+
ask mcp init --target cursor --yes
|
|
6468
6562
|
"""
|
|
6469
6563
|
from sourcecode.mcp.onboarding.detector import detect_clients, is_client_running
|
|
6470
6564
|
from sourcecode.mcp.onboarding.planner import build_install_plan
|
|
@@ -6509,7 +6603,7 @@ def mcp_init(
|
|
|
6509
6603
|
for a in already_done:
|
|
6510
6604
|
typer.echo(f" ✓ {a.client.name} {a.client.config_path}")
|
|
6511
6605
|
typer.echo("")
|
|
6512
|
-
typer.echo("Nothing to do. Remove:
|
|
6606
|
+
typer.echo("Nothing to do. Remove: ask mcp remove")
|
|
6513
6607
|
raise typer.Exit(code=0)
|
|
6514
6608
|
|
|
6515
6609
|
if already_done:
|
|
@@ -6566,7 +6660,7 @@ def mcp_init(
|
|
|
6566
6660
|
if not is_client_running(a.client):
|
|
6567
6661
|
typer.echo(
|
|
6568
6662
|
f" ⚠ Config written but {a.client.name} is not running. "
|
|
6569
|
-
f"Start {a.client.name} and run
|
|
6663
|
+
f"Start {a.client.name} and run ask mcp status to verify.",
|
|
6570
6664
|
err=False,
|
|
6571
6665
|
)
|
|
6572
6666
|
else:
|
|
@@ -6574,7 +6668,7 @@ def mcp_init(
|
|
|
6574
6668
|
typer.echo(f" ✓ {a.client.name} is running.{restart_msg}")
|
|
6575
6669
|
|
|
6576
6670
|
typer.echo("")
|
|
6577
|
-
typer.echo(" Remove:
|
|
6671
|
+
typer.echo(" Remove: ask mcp remove")
|
|
6578
6672
|
|
|
6579
6673
|
# Clear nudge flag: next run finds is_installed=True → no nudge.
|
|
6580
6674
|
from sourcecode.mcp_nudge import clear_nudge_flag as _clear_nudge
|
|
@@ -6611,18 +6705,18 @@ def mcp_status() -> None:
|
|
|
6611
6705
|
if not clients:
|
|
6612
6706
|
typer.echo(" No MCP clients detected on this system.")
|
|
6613
6707
|
typer.echo(sep)
|
|
6614
|
-
typer.echo(" Setup:
|
|
6708
|
+
typer.echo(" Setup: ask mcp init")
|
|
6615
6709
|
raise typer.Exit(code=0)
|
|
6616
6710
|
|
|
6617
6711
|
# Stage 2: Config files — is sourcecode registered in the client's config?
|
|
6618
6712
|
# FIX-P0-6: "configured" and "running" are distinct, independent checks.
|
|
6619
6713
|
# Also detect external server installs (different Python/executable than CLI).
|
|
6620
|
-
typer.echo("Config (
|
|
6714
|
+
typer.echo("Config (ASK Engine registered in client config?)")
|
|
6621
6715
|
for client in clients:
|
|
6622
6716
|
if not client.app_installed:
|
|
6623
6717
|
typer.echo(f" {client.name:<20} ✗ app not found at expected path")
|
|
6624
6718
|
typer.echo(f" Expected: {client.config_path}")
|
|
6625
|
-
typer.echo(f" Fix:
|
|
6719
|
+
typer.echo(f" Fix: ask mcp init --target {client.slug}")
|
|
6626
6720
|
continue
|
|
6627
6721
|
config = applier.read_config(client.config_path)
|
|
6628
6722
|
if applier.is_installed(config):
|
|
@@ -6638,11 +6732,11 @@ def mcp_status() -> None:
|
|
|
6638
6732
|
or (_reg_args and _reg_args[:2] == ["mcp", "serve"])
|
|
6639
6733
|
)
|
|
6640
6734
|
if _is_builtin:
|
|
6641
|
-
typer.echo(f" Server: built-in (
|
|
6735
|
+
typer.echo(f" Server: built-in (ask mcp serve) version={_cli_version}")
|
|
6642
6736
|
else:
|
|
6643
6737
|
# External server — different Python or custom server.py
|
|
6644
6738
|
typer.echo(f" Server: ⚠ EXTERNAL — {_reg_cmd} {' '.join(_reg_args)}")
|
|
6645
|
-
# Try to get the external server's
|
|
6739
|
+
# Try to get the external server's ask version by finding the
|
|
6646
6740
|
# sourcecode binary relative to the registered Python executable,
|
|
6647
6741
|
# or falling back to probing the Python for the installed package.
|
|
6648
6742
|
_ext_ver: str = "unknown"
|
|
@@ -6676,18 +6770,18 @@ def mcp_status() -> None:
|
|
|
6676
6770
|
f"CLI version={_cli_version}"
|
|
6677
6771
|
)
|
|
6678
6772
|
typer.echo(
|
|
6679
|
-
" To fix:
|
|
6773
|
+
" To fix: ask mcp init (re-register using CLI built-in server)"
|
|
6680
6774
|
)
|
|
6681
6775
|
elif _ext_ver != "unknown":
|
|
6682
6776
|
typer.echo(f" External server version={_ext_ver} (matches CLI ✓)")
|
|
6683
6777
|
else:
|
|
6684
6778
|
typer.echo(
|
|
6685
6779
|
f" ⚠ Cannot verify external server version (CLI={_cli_version}). "
|
|
6686
|
-
"Re-run:
|
|
6780
|
+
"Re-run: ask mcp init to switch to built-in server."
|
|
6687
6781
|
)
|
|
6688
6782
|
else:
|
|
6689
|
-
typer.echo(f" {client.name:<20} ✗ not configured (app found, but
|
|
6690
|
-
typer.echo(f" Fix:
|
|
6783
|
+
typer.echo(f" {client.name:<20} ✗ not configured (app found, but ASK Engine entry missing)")
|
|
6784
|
+
typer.echo(f" Fix: ask mcp init --target {client.slug}")
|
|
6691
6785
|
typer.echo("")
|
|
6692
6786
|
|
|
6693
6787
|
# Build config state map for cross-check in Stage 3.
|
|
@@ -6715,19 +6809,19 @@ def mcp_status() -> None:
|
|
|
6715
6809
|
_action_required.append(client.name)
|
|
6716
6810
|
else:
|
|
6717
6811
|
typer.echo(f" {client.name:<20} ✗ not running")
|
|
6718
|
-
typer.echo(f" Fix: open {client.name}, then run
|
|
6812
|
+
typer.echo(f" Fix: open {client.name}, then run ask mcp status")
|
|
6719
6813
|
|
|
6720
6814
|
typer.echo(sep)
|
|
6721
6815
|
if _action_required:
|
|
6722
6816
|
for _name in _action_required:
|
|
6723
|
-
typer.echo(f" ⚠ ACTION REQUIRED: {_name} is running but
|
|
6724
|
-
typer.echo(" Run:
|
|
6817
|
+
typer.echo(f" ⚠ ACTION REQUIRED: {_name} is running but ASK Engine is not configured.")
|
|
6818
|
+
typer.echo(" Run: ask mcp init")
|
|
6725
6819
|
typer.echo("")
|
|
6726
6820
|
typer.echo(" Note: 'configured' and 'running' are checked independently.")
|
|
6727
6821
|
typer.echo(" A running app still needs restart after first-time config.")
|
|
6728
6822
|
typer.echo(" Path: repo_path must use forward slashes: C:/Users/... or /unix/path")
|
|
6729
|
-
typer.echo(" Setup:
|
|
6730
|
-
typer.echo(" Remove:
|
|
6823
|
+
typer.echo(" Setup: ask mcp init")
|
|
6824
|
+
typer.echo(" Remove: ask mcp remove")
|
|
6731
6825
|
|
|
6732
6826
|
|
|
6733
6827
|
@mcp_app.command("remove")
|
|
@@ -6738,7 +6832,7 @@ def mcp_remove(
|
|
|
6738
6832
|
|
|
6739
6833
|
\b
|
|
6740
6834
|
Backs up config files before modifying. Restores from backup when available,
|
|
6741
|
-
otherwise removes the
|
|
6835
|
+
otherwise removes the ASK Engine entry while preserving all other config.
|
|
6742
6836
|
"""
|
|
6743
6837
|
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
6744
6838
|
from sourcecode.mcp.onboarding.planner import build_remove_plan
|
|
@@ -6750,7 +6844,7 @@ def mcp_remove(
|
|
|
6750
6844
|
|
|
6751
6845
|
if not installed:
|
|
6752
6846
|
typer.echo("sourcecode MCP integration not found in any client config.")
|
|
6753
|
-
typer.echo(" Setup:
|
|
6847
|
+
typer.echo(" Setup: ask mcp init")
|
|
6754
6848
|
raise typer.Exit(code=0)
|
|
6755
6849
|
|
|
6756
6850
|
typer.echo("Remove sourcecode MCP integration from:")
|
|
@@ -6793,23 +6887,23 @@ def mcp_remove(
|
|
|
6793
6887
|
raise typer.Exit(code=1)
|
|
6794
6888
|
|
|
6795
6889
|
typer.echo("MCP integration removed.")
|
|
6796
|
-
typer.echo(" Re-add:
|
|
6890
|
+
typer.echo(" Re-add: ask mcp init")
|
|
6797
6891
|
|
|
6798
6892
|
|
|
6799
6893
|
@mcp_app.command("list-tools")
|
|
6800
6894
|
def mcp_list_tools(
|
|
6801
6895
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
6802
6896
|
) -> None:
|
|
6803
|
-
"""List all MCP tools exposed by the
|
|
6897
|
+
"""List all MCP tools exposed by the ASK Engine MCP server.
|
|
6804
6898
|
|
|
6805
6899
|
\b
|
|
6806
6900
|
Shows each tool name, its description, and the CLI command it maps to.
|
|
6807
|
-
Useful for discovering capabilities when using
|
|
6901
|
+
Useful for discovering capabilities when using ASK Engine as an MCP server.
|
|
6808
6902
|
|
|
6809
6903
|
\b
|
|
6810
6904
|
Examples:
|
|
6811
|
-
|
|
6812
|
-
|
|
6905
|
+
ask mcp list-tools
|
|
6906
|
+
ask mcp list-tools --json
|
|
6813
6907
|
"""
|
|
6814
6908
|
import asyncio
|
|
6815
6909
|
import json as _json
|
|
@@ -6832,8 +6926,8 @@ def mcp_list_tools(
|
|
|
6832
6926
|
desc_first_line = (t.description or "").strip().splitlines()[0] if t.description else ""
|
|
6833
6927
|
typer.echo(f" {t.name:<35} {desc_first_line}")
|
|
6834
6928
|
typer.echo("")
|
|
6835
|
-
typer.echo("Use:
|
|
6836
|
-
typer.echo("Use:
|
|
6929
|
+
typer.echo("Use: ask mcp serve — start MCP server on stdio")
|
|
6930
|
+
typer.echo("Use: ask mcp init — configure MCP client")
|
|
6837
6931
|
|
|
6838
6932
|
|
|
6839
6933
|
# ── Cache subcommands ─────────────────────────────────────────────────────────
|
|
@@ -7034,8 +7128,18 @@ def cache_freshness_cmd(
|
|
|
7034
7128
|
|
|
7035
7129
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
7036
7130
|
|
|
7131
|
+
def _stderr_is_interactive() -> bool:
|
|
7132
|
+
"""True when stderr is a human terminal — used to gate the alias deprecation
|
|
7133
|
+
line so non-interactive callers keep a clean (JSON-only) stderr."""
|
|
7134
|
+
try:
|
|
7135
|
+
return bool(sys.stderr.isatty())
|
|
7136
|
+
except Exception:
|
|
7137
|
+
return False
|
|
7138
|
+
|
|
7139
|
+
|
|
7037
7140
|
def main_entry() -> None:
|
|
7038
|
-
"""CLI entry point
|
|
7141
|
+
"""CLI entry point for both the canonical ``ask`` command and the deprecated
|
|
7142
|
+
``sourcecode`` compat alias — one implementation, no duplication.
|
|
7039
7143
|
|
|
7040
7144
|
Calls _preprocess_argv() before Typer/Click parses sys.argv so that
|
|
7041
7145
|
repository path tokens are extracted before Click's Group callback
|
|
@@ -7049,9 +7153,23 @@ def main_entry() -> None:
|
|
|
7049
7153
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
7050
7154
|
except Exception:
|
|
7051
7155
|
pass
|
|
7156
|
+
# Deprecation notice when invoked through the legacy `sourcecode` alias.
|
|
7157
|
+
# One line, and only on an interactive terminal — pipes/agents that still call
|
|
7158
|
+
# `sourcecode` keep clean stderr (error envelopes are JSON on stderr), so the
|
|
7159
|
+
# machine contract is untouched. Humans at a TTY get nudged toward `ask`.
|
|
7160
|
+
try:
|
|
7161
|
+
_invoked = os.path.basename(sys.argv[0] or "").lower()
|
|
7162
|
+
if _invoked.startswith("sourcecode") and _stderr_is_interactive():
|
|
7163
|
+
sys.stderr.write(
|
|
7164
|
+
"⚠ 'sourcecode' is a deprecated alias for 'ask' and will be removed "
|
|
7165
|
+
"in a future major release. Use 'ask' instead.\n"
|
|
7166
|
+
)
|
|
7167
|
+
except Exception:
|
|
7168
|
+
pass
|
|
7052
7169
|
_preprocess_argv()
|
|
7053
7170
|
try:
|
|
7054
|
-
|
|
7171
|
+
# prog_name pins usage/help to the canonical `ask`, whatever the alias.
|
|
7172
|
+
app(prog_name="ask")
|
|
7055
7173
|
finally:
|
|
7056
7174
|
# Best-effort "new version available" nudge. Only speaks on an
|
|
7057
7175
|
# interactive terminal; never blocks, raises, or affects exit status.
|