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/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]sourcecode activate <key>[/dim] to unlock Pro"
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: sourcecode[/dim] {plan_badge}
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
- sourcecode --compact high-signal summary (~2,500–4,000 tokens)
167
- sourcecode --compact --git-context include git hotspots and uncommitted files
168
- sourcecode --agent full structured JSON for AI agents
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
- sourcecode my-project --compact
181
- sourcecode . --compact --git-context --copy
182
- sourcecode . --changed-only --git-context
183
- sourcecode prepare-context onboard my-project
184
- sourcecode prepare-context delta . --since main
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]→ sourcecode activate <key>[/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="sourcecode",
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(" → sourcecode mcp init", err=True)
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"sourcecode {__version__}")
678
+ typer.echo(f"ask {__version__}")
677
679
  raise typer.Exit()
678
680
 
679
681
 
680
- # ANSI Shadow block wordmark, stacked "source" / "code" so it fits an 80-col
681
- # terminal (the single-line "sourcecode" is 83 wide and would wrap).
682
- _WELCOME_SOURCE = (
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
- ("sourcecode --compact", "repo summary"),
701
- ("sourcecode migrate-check", "upgrade readiness (free)"),
702
- ("sourcecode prepare-context onboard", "onboarding"),
703
- ("sourcecode mcp init", "connect IDE"),
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: sourcecode", "",
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(" sourcecode --help all commands")
710
+ lines.append(" ask --help all commands")
715
711
  if tier != "Pro":
716
- lines.append(" sourcecode activate <key> unlock Pro")
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
- code_w = max(len(s) for s in _WELCOME_CODE)
740
+ ask_w = max(len(s) for s in _WELCOME_ASK)
745
741
 
746
742
  t = Text()
747
- for ln in _WELCOME_SOURCE:
748
- t.append(ln + "\n", style="bold cyan")
749
- # Append version + tier to the right of the "code" block's middle rows.
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
- sourcecode --compact high-signal summary (recommended)
1020
- sourcecode --compact --git-context include git hotspots
1021
- sourcecode /path/to/repo --compact analyze specific path
1022
- sourcecode --agent agent-optimized output (full detail)
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
- sourcecode prepare-context explain
2781
- sourcecode prepare-context explain /path/to/repo
2782
- sourcecode prepare-context fix-bug
2783
- sourcecode prepare-context delta --since main
2784
- sourcecode prepare-context review-pr --since origin/main
2785
- sourcecode prepare-context review-pr . --since main --output review.json
2786
- sourcecode prepare-context onboard --llm-prompt
2787
- sourcecode prepare-context --task-help
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": "sourcecode prepare-context review-pr --since <ref>",
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: sourcecode telemetry 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: sourcecode telemetry disable")
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: sourcecode telemetry enable")
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
- sourcecode repo-ir
3478
- sourcecode repo-ir /path/to/repo --since HEAD~1
3479
- sourcecode repo-ir --files src/main/java/UserService.java
3480
- sourcecode repo-ir --since main --output ir.json
3481
- sourcecode repo-ir --since HEAD~3 --summary-only --output ir-small.json
3482
- sourcecode repo-ir --max-nodes 200 --max-edges 500
3483
- sourcecode repo-ir --output ir.json.gz --gzip
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 'sourcecode license' for details.
3705
+ monoliths. Run 'ask license' for details.
3709
3706
 
3710
3707
  \b
3711
3708
  Examples:
3712
- sourcecode impact UserService
3713
- sourcecode impact org.keycloak.services.DefaultKeycloakSession /path/to/keycloak
3714
- sourcecode impact UserService --depth 6 --output impact.json
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: sourcecode impact <target> [path]\n"
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: sourcecode impact OrderService .",
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: sourcecode impact <target> [path] — "
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
- sourcecode endpoints .
3900
- sourcecode endpoints /path/to/repo
3901
- sourcecode endpoints . --output endpoints.json
3902
- sourcecode endpoints . --format yaml
3903
- sourcecode endpoints . --path-prefix /v1/liquidacion
3904
- sourcecode endpoints . --controller LiquidacionJornada
3905
- sourcecode endpoints . --limit 10
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
- data = _extract_java_endpoints(target)
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
- sourcecode validation .
4485
- sourcecode validation . --gaps-only
4486
- sourcecode validation . --path-prefix /owners
4487
- sourcecode validation . --format yaml
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
- data = build_validation_surface(target, graph=ContextGraph.build_from_root(target))
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
- sourcecode spring-audit . --ci # exit 1 on any finding
4680
- sourcecode spring-audit . --ci --min-severity high # exit 1 only on high/critical
4681
- sourcecode spring-audit . --ci --format github-comment # Markdown PR comment + exit 1
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
- sourcecode spring-audit .
4686
- sourcecode spring-audit /path/to/repo
4687
- sourcecode spring-audit . --scope security
4688
- sourcecode spring-audit . --min-severity high
4689
- sourcecode spring-audit . --output audit.json
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
- sourcecode migrate-check .
4886
- sourcecode migrate-check . --compact bounded decision summary
4887
- sourcecode migrate-check /path/to/repo --format text
4888
- sourcecode migrate-check . --min-severity high
4889
- sourcecode migrate-check . --output migration.json
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
- report = run_migrate_check(file_list, target, min_severity=min_severity)
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
- sourcecode impact-chain OrderService .
5015
- sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
5016
- sourcecode impact-chain PaymentService . --depth 6 --output impact.json
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: sourcecode impact-chain OrderService .",
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
- sourcecode pr-impact --files changed_files.txt
5159
- sourcecode pr-impact /path/to/repo --files diff.txt --format json
5160
- sourcecode pr-impact --files changes.txt --output pr_report.txt
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 && sourcecode pr-impact . --files 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
- cir = ContextGraph.build(file_list, target).cir
5228
- model = SpringSemanticModel.build(cir)
5229
- report = run_pr_impact(cir, changed_files, root=target, model=model)
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
- sourcecode explain UserService
5295
- sourcecode explain OrderController /path/to/repo
5296
- sourcecode explain UserService --format json
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: sourcecode explain UserService .",
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="sourcecode explain requires a Java/Spring repository.",
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
- cir = ContextGraph.build(file_list, target).cir
5347
- model = SpringSemanticModel.build(cir)
5348
- explanation = explain_class(class_name, cir, model)
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
- sourcecode onboard .
5403
- sourcecode onboard /path/to/repo --llm-prompt
5404
- sourcecode onboard . --output onboard.json
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
- sourcecode impact <target> — What breaks if I touch this?
5409
- sourcecode review-pr — Risk-rank a pending PR
5410
- sourcecode modernize . — Where should I refactor first?
5411
- sourcecode fix-bug — Where does this symptom live?
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
- sourcecode review-pr --since origin/main
5472
- sourcecode review-pr . --since main --format github-comment
5473
- sourcecode review-pr . --since HEAD~3 --output review.json
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
- sourcecode impact <target> — Single-symbol blast radius
5478
- sourcecode onboard . — Full architecture onboarding
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
- sourcecode fix-bug --symptom "NullPointerException in UserService"
5532
- sourcecode fix-bug . --symptom "401 on /api/orders"
5533
- sourcecode fix-bug . --output bug-context.json
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
- sourcecode impact <target> — Propagate impact from a specific class
5538
- sourcecode onboard . — Full architecture context first
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: sourcecode fix-bug . --symptom {_path_str!r}",
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
- unreferenced = [n for n in nodes if n["fqn"] not in dispatched_fqns]
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
- sourcecode modernize .
5701
- sourcecode modernize /path/to/repo --output modernize.json
5792
+ ask modernize .
5793
+ ask modernize /path/to/repo --output modernize.json
5702
5794
 
5703
5795
  \b
5704
5796
  Related workflows:
5705
- sourcecode onboard . — Architecture overview first
5706
- sourcecode impact <target> — Verify impact before touching a hotspot
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
- # BUG #6 (Broadleaf field test): make the in_degree metric self-describing
5946
- # so it is not confused with `explain`'s caller count. They differ by design.
5947
- "high_coupling_nodes_note": (
5948
- "in_degree = raw count of incoming graph edges across ALL edge types "
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; mutual=true marks a "
5977
- "bidirectional (cyclic) dependency — the actual tangle that blocks "
5978
- "independent module extraction. Empty means no cross-subsystem "
5979
- "structural coupling was detected."
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
- + "Cross-module tangles indicate coupling worth decomposing."
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
- sourcecode rename-class . --from ServiceA --to ServiceB
6065
- sourcecode rename-class /path/to/repo --from OrderManager --to OrderService
6066
- sourcecode rename-class . --from OldName --to NewName --dry-run
6067
- sourcecode rename-class . --from OldName --to NewName --output rename-audit.json
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
- sourcecode chunk-file NominasCalculoService.java
6171
- sourcecode chunk-file BigService.java --max-lines 300
6172
- sourcecode chunk-file BigService.java --chunk 5 # read chunk 5 only
6173
- sourcecode chunk-file BigService.java --metadata-only # sizes/boundaries only
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
- sourcecode activate SC-XXXX-XXXX-XXXX
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"sourcecode {__version__}")
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"sourcecode {__version__}")
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(" sourcecode telemetry enable")
6325
- typer.echo(" sourcecode telemetry disable")
6326
- typer.echo(" sourcecode telemetry status")
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": "sourcecode cold-start (without --compact)"}
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 sourcecode server entry. Fully idempotent — safe to re-run.
6555
+ inserts the ASK Engine server entry. Fully idempotent — safe to re-run.
6462
6556
 
6463
6557
  \b
6464
6558
  Examples:
6465
- sourcecode mcp init
6466
- sourcecode mcp init --target claude-desktop
6467
- sourcecode mcp init --target cursor --yes
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: sourcecode mcp 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 sourcecode mcp status to verify.",
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: sourcecode mcp 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: sourcecode mcp init")
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 (sourcecode registered in client 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: sourcecode mcp init --target {client.slug}")
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 (sourcecode mcp serve) version={_cli_version}")
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 sourcecode version by finding the
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: sourcecode mcp init (re-register using CLI built-in server)"
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: sourcecode mcp init to switch to built-in server."
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 sourcecode entry missing)")
6690
- typer.echo(f" Fix: sourcecode mcp init --target {client.slug}")
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 sourcecode mcp status")
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 sourcecode is not configured.")
6724
- typer.echo(" Run: sourcecode mcp init")
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: sourcecode mcp init")
6730
- typer.echo(" Remove: sourcecode mcp 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 sourcecode entry while preserving all other config.
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: sourcecode mcp init")
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: sourcecode mcp init")
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 sourcecode server.
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 sourcecode as an MCP server.
6901
+ Useful for discovering capabilities when using ASK Engine as an MCP server.
6808
6902
 
6809
6903
  \b
6810
6904
  Examples:
6811
- sourcecode mcp list-tools
6812
- sourcecode mcp list-tools --json
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: sourcecode mcp serve — start MCP server on stdio")
6836
- typer.echo("Use: sourcecode mcp init — configure MCP client")
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
- app()
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.