codegraph-engine 2.1.7__py3-none-any.whl → 2.1.8__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.
- codegraph/__init__.py +1 -1
- codegraph/cli.py +124 -47
- codegraph/cli_output.py +190 -0
- codegraph/context.py +116 -3
- codegraph/indexing/indexer.py +7 -3
- codegraph/installer.py +72 -22
- codegraph/mcp/server.py +89 -5
- codegraph/mcp_diagnostics.py +5 -2
- codegraph/monorepo.py +1 -1
- codegraph/process_lifecycle.py +1115 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/METADATA +44 -19
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/RECORD +16 -14
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.1.7.dist-info → codegraph_engine-2.1.8.dist-info}/top_level.txt +0 -0
codegraph/__init__.py
CHANGED
codegraph/cli.py
CHANGED
|
@@ -9,6 +9,7 @@ import typer
|
|
|
9
9
|
|
|
10
10
|
from codegraph import __version__
|
|
11
11
|
from codegraph.architecture import get_architecture
|
|
12
|
+
from codegraph.cli_output import cli_echo
|
|
12
13
|
from codegraph.config import Settings
|
|
13
14
|
from codegraph.context import get_context
|
|
14
15
|
from codegraph.errors import ErrorCode, SecurityError
|
|
@@ -605,8 +606,10 @@ def serve(
|
|
|
605
606
|
"""Run the stdio MCP server (requires the optional mcp extra)."""
|
|
606
607
|
repo = _resolve_repo(path, repository)
|
|
607
608
|
from codegraph.mcp import create_server
|
|
609
|
+
from codegraph.process_lifecycle import run_mcp_stdio_server
|
|
608
610
|
|
|
609
|
-
create_server(repo, profile=profile)
|
|
611
|
+
server = create_server(repo, profile=profile)
|
|
612
|
+
run_mcp_stdio_server(server, repo, profile=profile)
|
|
610
613
|
|
|
611
614
|
|
|
612
615
|
mcp_app = typer.Typer(help="MCP server commands.")
|
|
@@ -622,8 +625,70 @@ def mcp_serve(
|
|
|
622
625
|
"""Run the stdio MCP server (requires the optional mcp extra)."""
|
|
623
626
|
repo = _resolve_repo(path, repository)
|
|
624
627
|
from codegraph.mcp import create_server
|
|
628
|
+
from codegraph.process_lifecycle import run_mcp_stdio_server
|
|
625
629
|
|
|
626
|
-
create_server(repo, profile=profile)
|
|
630
|
+
server = create_server(repo, profile=profile)
|
|
631
|
+
run_mcp_stdio_server(server, repo, profile=profile)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _execute_stop(
|
|
635
|
+
path: Path | None,
|
|
636
|
+
repository: Path | None,
|
|
637
|
+
all_processes: bool,
|
|
638
|
+
timeout: float,
|
|
639
|
+
json_output: bool,
|
|
640
|
+
) -> None:
|
|
641
|
+
from codegraph.process_lifecycle import stop_codegraph_processes
|
|
642
|
+
|
|
643
|
+
target_repo: Path | None = None
|
|
644
|
+
if not all_processes:
|
|
645
|
+
target_repo = _resolve_repo(path, repository)
|
|
646
|
+
|
|
647
|
+
res = stop_codegraph_processes(
|
|
648
|
+
repository=target_repo,
|
|
649
|
+
stop_all=all_processes,
|
|
650
|
+
timeout_sec=timeout,
|
|
651
|
+
)
|
|
652
|
+
if json_output:
|
|
653
|
+
cli_echo(json.dumps(res.as_dict(), indent=2), json_mode=True)
|
|
654
|
+
else:
|
|
655
|
+
cli_echo(res.format_human())
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@app.command("stop")
|
|
659
|
+
def stop_cmd(
|
|
660
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
661
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
662
|
+
all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
|
|
663
|
+
timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 3.0,
|
|
664
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Safely stop running CodeGraph MCP background processes and release file locks."""
|
|
667
|
+
_execute_stop(path, repository, all_processes, timeout, json_output)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@mcp_app.command("stop")
|
|
671
|
+
def mcp_stop_cmd(
|
|
672
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
673
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
674
|
+
all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
|
|
675
|
+
timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 3.0,
|
|
676
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
|
|
677
|
+
) -> None:
|
|
678
|
+
"""Safely stop running CodeGraph MCP background processes and release file locks."""
|
|
679
|
+
_execute_stop(path, repository, all_processes, timeout, json_output)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@mcp_app.command("kill")
|
|
683
|
+
def mcp_kill_cmd(
|
|
684
|
+
path: Annotated[Path | None, typer.Argument(help="Repository path (default: current directory)")] = None,
|
|
685
|
+
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
686
|
+
all_processes: Annotated[bool, typer.Option("--all", "-a", help="Stop all CodeGraph-owned MCP processes across all repositories")] = False,
|
|
687
|
+
timeout: Annotated[float, typer.Option("--timeout", help="Graceful shutdown timeout in seconds before force-kill")] = 1.0,
|
|
688
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output deterministic machine-readable JSON")] = False,
|
|
689
|
+
) -> None:
|
|
690
|
+
"""Alias for `codegraph stop` to terminate CodeGraph MCP processes and release locks."""
|
|
691
|
+
_execute_stop(path, repository, all_processes, timeout, json_output)
|
|
627
692
|
|
|
628
693
|
|
|
629
694
|
@mcp_app.command("doctor")
|
|
@@ -636,9 +701,9 @@ def mcp_doctor(
|
|
|
636
701
|
|
|
637
702
|
report = run_mcp_doctor(profile=profile)
|
|
638
703
|
if json_output:
|
|
639
|
-
|
|
704
|
+
cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
|
|
640
705
|
else:
|
|
641
|
-
|
|
706
|
+
cli_echo(report.format_human())
|
|
642
707
|
if not report.healthy:
|
|
643
708
|
raise typer.Exit(code=1)
|
|
644
709
|
|
|
@@ -656,9 +721,9 @@ def mcp_config_check(
|
|
|
656
721
|
ws = (path or Path.cwd()).resolve()
|
|
657
722
|
rep = check_mcp_configuration(workspace_dir=ws, config_file=config_file, include_example=include_example)
|
|
658
723
|
if json_output:
|
|
659
|
-
|
|
724
|
+
cli_echo(json.dumps(rep.as_dict(), indent=2), json_mode=True)
|
|
660
725
|
else:
|
|
661
|
-
|
|
726
|
+
cli_echo(f"{rep.status.value}: {rep.message}")
|
|
662
727
|
|
|
663
728
|
|
|
664
729
|
@mcp_app.command("capabilities")
|
|
@@ -666,7 +731,7 @@ def mcp_capabilities() -> None:
|
|
|
666
731
|
"""Output the deterministic machine-readable CodeGraph capability manifest."""
|
|
667
732
|
from codegraph.agent_capabilities import get_capability_manifest
|
|
668
733
|
|
|
669
|
-
|
|
734
|
+
cli_echo(json.dumps(get_capability_manifest(), indent=2), json_mode=True)
|
|
670
735
|
|
|
671
736
|
|
|
672
737
|
@mcp_app.command("rules")
|
|
@@ -676,7 +741,7 @@ def mcp_rules(
|
|
|
676
741
|
"""Render deterministic CodeGraph agent rules for the specified agent."""
|
|
677
742
|
from codegraph.agent_rules import render_agent_rules
|
|
678
743
|
|
|
679
|
-
|
|
744
|
+
cli_echo(render_agent_rules(agent=agent), nl=False)
|
|
680
745
|
|
|
681
746
|
|
|
682
747
|
@app.command()
|
|
@@ -685,9 +750,20 @@ def doctor(
|
|
|
685
750
|
repository: Annotated[Path | None, typer.Option("--repository", "-r", "--repo", help="Repository path")] = None,
|
|
686
751
|
resources: Annotated[bool, typer.Option("--resources")] = False,
|
|
687
752
|
database: Annotated[bool, typer.Option("--database", help="Run comprehensive database integrity and schema checks")] = False,
|
|
753
|
+
processes: Annotated[bool, typer.Option("--processes", help="Inspect active CodeGraph processes, parent state, and stale PID files")] = False,
|
|
688
754
|
json_output: Annotated[bool, typer.Option("--json")] = False,
|
|
689
755
|
) -> None:
|
|
690
|
-
"""Check repository and index readiness, database integrity, and freshness."""
|
|
756
|
+
"""Check repository and index readiness, database integrity, process lifecycle, and freshness."""
|
|
757
|
+
if processes:
|
|
758
|
+
from codegraph.process_lifecycle import discover_codegraph_processes
|
|
759
|
+
|
|
760
|
+
discovery = discover_codegraph_processes(clean_stale=True)
|
|
761
|
+
if json_output:
|
|
762
|
+
cli_echo(json.dumps(discovery.as_dict(), indent=2), json_mode=True)
|
|
763
|
+
else:
|
|
764
|
+
cli_echo(discovery.format_human())
|
|
765
|
+
return
|
|
766
|
+
|
|
691
767
|
repo = _resolve_repo(path, repository)
|
|
692
768
|
indexer_inst = _indexer(repo)
|
|
693
769
|
db = repo / ".codegraph.sqlite3"
|
|
@@ -1007,7 +1083,7 @@ def install(
|
|
|
1007
1083
|
|
|
1008
1084
|
rules_txt = render_agent_rules(canon)
|
|
1009
1085
|
if json_output:
|
|
1010
|
-
|
|
1086
|
+
cli_echo(
|
|
1011
1087
|
json.dumps(
|
|
1012
1088
|
{
|
|
1013
1089
|
"status": "ok",
|
|
@@ -1016,21 +1092,22 @@ def install(
|
|
|
1016
1092
|
"instructions_block": format_marker_block(rules_txt),
|
|
1017
1093
|
},
|
|
1018
1094
|
indent=2,
|
|
1019
|
-
)
|
|
1095
|
+
),
|
|
1096
|
+
json_mode=True,
|
|
1020
1097
|
)
|
|
1021
1098
|
else:
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1099
|
+
cli_echo(f"# MCP Configuration ({canon})\n")
|
|
1100
|
+
cli_echo(json.dumps(snippet, indent=2), json_mode=True)
|
|
1101
|
+
cli_echo(f"\n# Instructions Block ({canon})\n")
|
|
1102
|
+
cli_echo(format_marker_block(rules_txt), nl=False)
|
|
1026
1103
|
return
|
|
1027
1104
|
except CodeGraphError as exc:
|
|
1028
1105
|
if json_output:
|
|
1029
|
-
|
|
1106
|
+
cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
|
|
1030
1107
|
else:
|
|
1031
|
-
|
|
1108
|
+
cli_echo(f"Error [{exc.code}]: {exc.message}")
|
|
1032
1109
|
if exc.next_action and exc.next_action.get("command"):
|
|
1033
|
-
|
|
1110
|
+
cli_echo(f"\nTry:\n {exc.next_action['command']}")
|
|
1034
1111
|
raise typer.Exit(code=1) from exc
|
|
1035
1112
|
|
|
1036
1113
|
ws = _resolve_repo(path, repository)
|
|
@@ -1040,15 +1117,15 @@ def install(
|
|
|
1040
1117
|
|
|
1041
1118
|
assert isinstance(preview_rep, InstallerReport)
|
|
1042
1119
|
if not json_output:
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1120
|
+
cli_echo(preview_rep.format_preview_human())
|
|
1121
|
+
cli_echo("")
|
|
1122
|
+
cli_echo("Continue? [y/N] ", nl=False)
|
|
1046
1123
|
try:
|
|
1047
1124
|
ans = input().strip().lower()
|
|
1048
1125
|
except EOFError:
|
|
1049
1126
|
ans = ""
|
|
1050
1127
|
if not json_output:
|
|
1051
|
-
|
|
1128
|
+
cli_echo("")
|
|
1052
1129
|
return ans in ("y", "yes")
|
|
1053
1130
|
|
|
1054
1131
|
try:
|
|
@@ -1062,22 +1139,22 @@ def install(
|
|
|
1062
1139
|
)
|
|
1063
1140
|
except CodeGraphError as exc:
|
|
1064
1141
|
if json_output:
|
|
1065
|
-
|
|
1142
|
+
cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
|
|
1066
1143
|
else:
|
|
1067
|
-
|
|
1144
|
+
cli_echo(f"Error [{exc.code}]: {exc.message}")
|
|
1068
1145
|
if exc.next_action and exc.next_action.get("reason"):
|
|
1069
|
-
|
|
1146
|
+
cli_echo(f"\nExpected:\n {exc.next_action['reason']}")
|
|
1070
1147
|
if exc.next_action and exc.next_action.get("command"):
|
|
1071
|
-
|
|
1148
|
+
cli_echo(f"\nTry:\n {exc.next_action['command']}")
|
|
1072
1149
|
raise typer.Exit(code=1) from exc
|
|
1073
1150
|
|
|
1074
1151
|
if json_output:
|
|
1075
|
-
|
|
1152
|
+
cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
|
|
1076
1153
|
else:
|
|
1077
1154
|
if yes and not dry_run:
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1155
|
+
cli_echo(report.format_preview_human())
|
|
1156
|
+
cli_echo("")
|
|
1157
|
+
cli_echo(report.format_human())
|
|
1081
1158
|
|
|
1082
1159
|
if report.status == "error":
|
|
1083
1160
|
raise typer.Exit(code=1)
|
|
@@ -1107,17 +1184,17 @@ def uninstall(
|
|
|
1107
1184
|
|
|
1108
1185
|
def _confirm_uninstall(preview_rep: InstallerReport) -> bool:
|
|
1109
1186
|
if not json_output:
|
|
1110
|
-
|
|
1187
|
+
cli_echo("CodeGraph Uninstaller\n")
|
|
1111
1188
|
for c in preview_rep.changes:
|
|
1112
1189
|
if c.action in ("remove", "modify"):
|
|
1113
|
-
|
|
1114
|
-
|
|
1190
|
+
cli_echo(f" - {c.display_path} ({c.action})")
|
|
1191
|
+
cli_echo("\nContinue? [y/N] ", nl=False)
|
|
1115
1192
|
try:
|
|
1116
1193
|
ans = input().strip().lower()
|
|
1117
1194
|
except EOFError:
|
|
1118
1195
|
ans = ""
|
|
1119
1196
|
if not json_output:
|
|
1120
|
-
|
|
1197
|
+
cli_echo("")
|
|
1121
1198
|
return ans in ("y", "yes")
|
|
1122
1199
|
|
|
1123
1200
|
try:
|
|
@@ -1130,17 +1207,17 @@ def uninstall(
|
|
|
1130
1207
|
)
|
|
1131
1208
|
except CodeGraphError as exc:
|
|
1132
1209
|
if json_output:
|
|
1133
|
-
|
|
1210
|
+
cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
|
|
1134
1211
|
else:
|
|
1135
|
-
|
|
1212
|
+
cli_echo(f"Error [{exc.code}]: {exc.message}")
|
|
1136
1213
|
if exc.next_action and exc.next_action.get("command"):
|
|
1137
|
-
|
|
1214
|
+
cli_echo(f"\nTry:\n {exc.next_action['command']}")
|
|
1138
1215
|
raise typer.Exit(code=1) from exc
|
|
1139
1216
|
|
|
1140
1217
|
if json_output:
|
|
1141
|
-
|
|
1218
|
+
cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
|
|
1142
1219
|
else:
|
|
1143
|
-
|
|
1220
|
+
cli_echo(report.format_human())
|
|
1144
1221
|
|
|
1145
1222
|
|
|
1146
1223
|
@app.command()
|
|
@@ -1159,16 +1236,16 @@ def uninit(
|
|
|
1159
1236
|
|
|
1160
1237
|
def _confirm_uninit(preview_rep: InstallerReport) -> bool:
|
|
1161
1238
|
if not json_output:
|
|
1162
|
-
|
|
1239
|
+
cli_echo("Remove CodeGraph project index files?\n")
|
|
1163
1240
|
for c in preview_rep.changes:
|
|
1164
|
-
|
|
1165
|
-
|
|
1241
|
+
cli_echo(f" - {c.display_path}")
|
|
1242
|
+
cli_echo("\nContinue? [y/N] ", nl=False)
|
|
1166
1243
|
try:
|
|
1167
1244
|
ans = input().strip().lower()
|
|
1168
1245
|
except EOFError:
|
|
1169
1246
|
ans = ""
|
|
1170
1247
|
if not json_output:
|
|
1171
|
-
|
|
1248
|
+
cli_echo("")
|
|
1172
1249
|
return ans in ("y", "yes")
|
|
1173
1250
|
|
|
1174
1251
|
try:
|
|
@@ -1179,15 +1256,15 @@ def uninit(
|
|
|
1179
1256
|
)
|
|
1180
1257
|
except CodeGraphError as exc:
|
|
1181
1258
|
if json_output:
|
|
1182
|
-
|
|
1259
|
+
cli_echo(json.dumps(exc.to_response(), indent=2), json_mode=True)
|
|
1183
1260
|
else:
|
|
1184
|
-
|
|
1261
|
+
cli_echo(f"Error [{exc.code}]: {exc.message}")
|
|
1185
1262
|
raise typer.Exit(code=1) from exc
|
|
1186
1263
|
|
|
1187
1264
|
if json_output:
|
|
1188
|
-
|
|
1265
|
+
cli_echo(json.dumps(report.as_dict(), indent=2), json_mode=True)
|
|
1189
1266
|
else:
|
|
1190
|
-
|
|
1267
|
+
cli_echo(report.format_human())
|
|
1191
1268
|
|
|
1192
1269
|
|
|
1193
1270
|
if __name__ == "__main__":
|
codegraph/cli_output.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Centralized cross-platform CLI output utility for Windows/macOS/Linux encoding safety.
|
|
2
|
+
|
|
3
|
+
Guarantees:
|
|
4
|
+
- Never assumes stdout/stderr is UTF-8 (safe on Windows cp1252, cp437, ASCII, and redirected pipes).
|
|
5
|
+
- Preserves rich Unicode symbols (`✓`, `✗`, `⚠`, `↻`, `(•)`) when the target stream supports them.
|
|
6
|
+
- Automatically degrades to deterministic ASCII markers (`[OK]`, `[ERROR]`, `[WARN]`, `[REPAIRED]`, `(*)`)
|
|
7
|
+
when the stream encoding does not support Unicode box/status glyphs.
|
|
8
|
+
- Ensures `--json` output is strictly machine-readable and never contains decorative Unicode glyphs.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import codecs
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, TextIO
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
_PROBE_GLYPHS = "✓✗⚠↻•→↓─—–"
|
|
21
|
+
|
|
22
|
+
# Ordered replacements so multi-char patterns (e.g. "(•)") are replaced before single chars ("•")
|
|
23
|
+
_UNICODE_TO_ASCII_MAP: tuple[tuple[str, str], ...] = (
|
|
24
|
+
("(•)", "(*)"),
|
|
25
|
+
("✓", "[OK]"),
|
|
26
|
+
("✗", "[ERROR]"),
|
|
27
|
+
("⚠", "[WARN]"),
|
|
28
|
+
("↻", "[REPAIRED]"),
|
|
29
|
+
("•", "*"),
|
|
30
|
+
("→", "->"),
|
|
31
|
+
("↓", "v"),
|
|
32
|
+
("—", "--"),
|
|
33
|
+
("–", "-"),
|
|
34
|
+
("─", "-"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class CliSymbols:
|
|
40
|
+
"""Encoding-aware CLI status symbols."""
|
|
41
|
+
|
|
42
|
+
unicode_enabled: bool
|
|
43
|
+
ok: str
|
|
44
|
+
error: str
|
|
45
|
+
warn: str
|
|
46
|
+
repair: str
|
|
47
|
+
radio_on: str
|
|
48
|
+
radio_off: str
|
|
49
|
+
arrow_right: str
|
|
50
|
+
arrow_down: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
UNICODE_SYMBOLS = CliSymbols(
|
|
54
|
+
unicode_enabled=True,
|
|
55
|
+
ok="✓",
|
|
56
|
+
error="✗",
|
|
57
|
+
warn="⚠",
|
|
58
|
+
repair="↻",
|
|
59
|
+
radio_on="(•)",
|
|
60
|
+
radio_off="( )",
|
|
61
|
+
arrow_right="→",
|
|
62
|
+
arrow_down="↓",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
ASCII_SYMBOLS = CliSymbols(
|
|
66
|
+
unicode_enabled=False,
|
|
67
|
+
ok="[OK]",
|
|
68
|
+
error="[ERROR]",
|
|
69
|
+
warn="[WARN]",
|
|
70
|
+
repair="[REPAIRED]",
|
|
71
|
+
radio_on="(*)",
|
|
72
|
+
radio_off="( )",
|
|
73
|
+
arrow_right="->",
|
|
74
|
+
arrow_down="v",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_stream_encoding(stream: Any = None) -> str:
|
|
79
|
+
"""Safely detect the character encoding of `stream` (defaults to `sys.stdout`)."""
|
|
80
|
+
target = stream if stream is not None else sys.stdout
|
|
81
|
+
enc = getattr(target, "encoding", None)
|
|
82
|
+
if not enc or not isinstance(enc, str):
|
|
83
|
+
return "ascii"
|
|
84
|
+
try:
|
|
85
|
+
info = codecs.lookup(enc)
|
|
86
|
+
return info.name
|
|
87
|
+
except LookupError:
|
|
88
|
+
return "ascii"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def supports_unicode(
|
|
92
|
+
stream: Any = None,
|
|
93
|
+
*,
|
|
94
|
+
unicode_override: bool | None = None,
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""Return True if `stream` can encode all CodeGraph Unicode status glyphs without error."""
|
|
97
|
+
if unicode_override is not None:
|
|
98
|
+
return unicode_override
|
|
99
|
+
|
|
100
|
+
env_ascii = os.environ.get("CODEGRAPH_ASCII_OUTPUT", "").strip().lower()
|
|
101
|
+
if env_ascii in ("1", "true", "yes", "on"):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
env_unicode = os.environ.get("CODEGRAPH_UNICODE_OUTPUT", "").strip().lower()
|
|
105
|
+
if env_unicode in ("1", "true", "yes", "on"):
|
|
106
|
+
return True
|
|
107
|
+
if env_unicode in ("0", "false", "no", "off"):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
enc = detect_stream_encoding(stream)
|
|
111
|
+
try:
|
|
112
|
+
_PROBE_GLYPHS.encode(enc, errors="strict")
|
|
113
|
+
return True
|
|
114
|
+
except (UnicodeEncodeError, LookupError):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_cli_symbols(
|
|
119
|
+
stream: Any = None,
|
|
120
|
+
*,
|
|
121
|
+
unicode_override: bool | None = None,
|
|
122
|
+
) -> CliSymbols:
|
|
123
|
+
"""Return `UNICODE_SYMBOLS` when supported by `stream`, else `ASCII_SYMBOLS`."""
|
|
124
|
+
if supports_unicode(stream, unicode_override=unicode_override):
|
|
125
|
+
return UNICODE_SYMBOLS
|
|
126
|
+
return ASCII_SYMBOLS
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def sanitize_text_for_stream(
|
|
130
|
+
text: str,
|
|
131
|
+
stream: Any = None,
|
|
132
|
+
*,
|
|
133
|
+
unicode_override: bool | None = None,
|
|
134
|
+
) -> str:
|
|
135
|
+
"""Format `text` safely for `stream`, converting decorative Unicode when unsupported."""
|
|
136
|
+
if not text:
|
|
137
|
+
return text
|
|
138
|
+
|
|
139
|
+
target = stream if stream is not None else sys.stdout
|
|
140
|
+
use_unicode = supports_unicode(target, unicode_override=unicode_override)
|
|
141
|
+
out = text
|
|
142
|
+
if not use_unicode:
|
|
143
|
+
for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
|
|
144
|
+
if uni_tok in out:
|
|
145
|
+
out = out.replace(uni_tok, ascii_tok)
|
|
146
|
+
|
|
147
|
+
enc = detect_stream_encoding(target)
|
|
148
|
+
try:
|
|
149
|
+
out.encode(enc, errors="strict")
|
|
150
|
+
return out
|
|
151
|
+
except (UnicodeEncodeError, LookupError):
|
|
152
|
+
# Replace any remaining unencodable glyphs cleanly so write() never raises UnicodeEncodeError
|
|
153
|
+
for uni_tok, ascii_tok in _UNICODE_TO_ASCII_MAP:
|
|
154
|
+
if uni_tok in out:
|
|
155
|
+
out = out.replace(uni_tok, ascii_tok)
|
|
156
|
+
return out.encode(enc, errors="replace").decode(enc, errors="replace")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sanitize_json_string(raw_json: str, stream: Any = None) -> str:
|
|
160
|
+
"""Ensure machine-readable JSON output never crashes non-UTF-8 streams."""
|
|
161
|
+
enc = detect_stream_encoding(stream)
|
|
162
|
+
try:
|
|
163
|
+
raw_json.encode(enc, errors="strict")
|
|
164
|
+
return raw_json
|
|
165
|
+
except (UnicodeEncodeError, LookupError):
|
|
166
|
+
# Escape non-ASCII characters using standard JSON \uXXXX escapes
|
|
167
|
+
return raw_json.encode("unicode_escape").decode("ascii")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cli_echo(
|
|
171
|
+
message: str = "",
|
|
172
|
+
*,
|
|
173
|
+
nl: bool = True,
|
|
174
|
+
err: bool = False,
|
|
175
|
+
json_mode: bool = False,
|
|
176
|
+
stream: TextIO | None = None,
|
|
177
|
+
unicode_override: bool | None = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Centralized safe CLI output function replacing raw `typer.echo`."""
|
|
180
|
+
target = stream if stream is not None else (sys.stderr if err else sys.stdout)
|
|
181
|
+
if json_mode:
|
|
182
|
+
safe_msg = sanitize_json_string(str(message), target)
|
|
183
|
+
else:
|
|
184
|
+
safe_msg = sanitize_text_for_stream(str(message), target, unicode_override=unicode_override)
|
|
185
|
+
|
|
186
|
+
if stream is not None:
|
|
187
|
+
stream.write(safe_msg + ("\n" if nl else ""))
|
|
188
|
+
stream.flush()
|
|
189
|
+
else:
|
|
190
|
+
typer.echo(safe_msg, nl=nl, err=err)
|
codegraph/context.py
CHANGED
|
@@ -708,6 +708,9 @@ def _get_context_impl(
|
|
|
708
708
|
|
|
709
709
|
stage1_candidate_count = len(candidate_dicts)
|
|
710
710
|
|
|
711
|
+
# Instantiate ConstraintGuard for hard safety invariants
|
|
712
|
+
_guard = ConstraintGuard.from_task_spec(task_spec)
|
|
713
|
+
|
|
711
714
|
# Process framework routes
|
|
712
715
|
route_relationships: list[Relationship] = []
|
|
713
716
|
entry_points_out: list[dict[str, object]] = []
|
|
@@ -717,16 +720,26 @@ def _get_context_impl(
|
|
|
717
720
|
r_path = r["route_path"]
|
|
718
721
|
r_method = r["http_method"]
|
|
719
722
|
ep_id = r["endpoint_id"] or f"{r_method} {r_path}"
|
|
723
|
+
r_segments = [
|
|
724
|
+
seg.lower()
|
|
725
|
+
for seg in str(r_path).split("/")
|
|
726
|
+
if len(seg) >= 4
|
|
727
|
+
and not seg.startswith("{")
|
|
728
|
+
and not seg.startswith("<")
|
|
729
|
+
and seg.lower() not in {"api", "v1", "v2"}
|
|
730
|
+
]
|
|
720
731
|
matches_task = (
|
|
721
732
|
r_path in task_display_str
|
|
722
733
|
or ep_id.lower() in task_display_str.lower()
|
|
723
734
|
or (r_method in task_display_str.upper() and r_path in task_display_str)
|
|
735
|
+
or task_spec.intent == "ARCHITECTURE"
|
|
736
|
+
or any(seg in task_display_str.lower() for seg in r_segments)
|
|
724
737
|
or any(
|
|
725
738
|
(len(tgt) >= 3 and (tgt.lower() == r["handler_name"].lower() or tgt.lower() in r_path.lower()))
|
|
726
739
|
for tgt in task_spec.targets
|
|
727
740
|
)
|
|
728
741
|
)
|
|
729
|
-
if matches_task:
|
|
742
|
+
if matches_task and _guard.allows_symbol(str(r["handler_name"])) and _guard.allows_file(str(r["file_path"])):
|
|
730
743
|
target_symbols.add(r["handler_name"])
|
|
731
744
|
r_line = r["line"] or 1
|
|
732
745
|
candidate_dicts.append(
|
|
@@ -799,8 +812,108 @@ def _get_context_impl(
|
|
|
799
812
|
except Exception:
|
|
800
813
|
pass
|
|
801
814
|
|
|
802
|
-
#
|
|
803
|
-
|
|
815
|
+
# Seed architectural boundary classes and gateway methods for ARCHITECTURE intent
|
|
816
|
+
if task_spec.intent == "ARCHITECTURE":
|
|
817
|
+
try:
|
|
818
|
+
arch_rows = con.execute(
|
|
819
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
820
|
+
"FROM symbols WHERE kind = 'class' OR name = 'app'"
|
|
821
|
+
).fetchall()
|
|
822
|
+
for a_row in arch_rows:
|
|
823
|
+
a_name = str(a_row["name"])
|
|
824
|
+
a_sym = str(a_row["qualified_name"] or a_name)
|
|
825
|
+
a_cid = str(a_row["canonical_id"] or "")
|
|
826
|
+
a_file = str(a_row["path"] or "")
|
|
827
|
+
if (
|
|
828
|
+
_guard.allows_symbol(a_sym)
|
|
829
|
+
and _guard.allows_symbol(a_name)
|
|
830
|
+
and (not a_cid or _guard.allows_canonical_id(a_cid))
|
|
831
|
+
and _guard.allows_file(a_file)
|
|
832
|
+
):
|
|
833
|
+
target_symbols.add(a_name)
|
|
834
|
+
target_symbols.add(a_sym)
|
|
835
|
+
candidate_dicts.append(
|
|
836
|
+
{
|
|
837
|
+
"file": a_file,
|
|
838
|
+
"symbol": a_name,
|
|
839
|
+
"canonical_id": a_cid,
|
|
840
|
+
"start_line": int(a_row["start_line"] or 1),
|
|
841
|
+
"end_line": int(a_row["end_line"] or 1),
|
|
842
|
+
"score": 0.90,
|
|
843
|
+
"relationship": "DEFINES",
|
|
844
|
+
"snippet": f"Architectural symbol {a_sym} ({a_row['kind']}) in {a_file}",
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
if "gateway" in task_display_str.lower():
|
|
848
|
+
gw_rows = con.execute(
|
|
849
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
850
|
+
"FROM symbols WHERE qualified_name LIKE '%Gateway.%'"
|
|
851
|
+
).fetchall()
|
|
852
|
+
for g_row in gw_rows:
|
|
853
|
+
g_name = str(g_row["name"])
|
|
854
|
+
g_sym = str(g_row["qualified_name"] or g_name)
|
|
855
|
+
g_cid = str(g_row["canonical_id"] or "")
|
|
856
|
+
g_file = str(g_row["path"] or "")
|
|
857
|
+
if (
|
|
858
|
+
_guard.allows_symbol(g_sym)
|
|
859
|
+
and _guard.allows_symbol(g_name)
|
|
860
|
+
and (not g_cid or _guard.allows_canonical_id(g_cid))
|
|
861
|
+
and _guard.allows_file(g_file)
|
|
862
|
+
):
|
|
863
|
+
target_symbols.add(g_name)
|
|
864
|
+
target_symbols.add(g_sym)
|
|
865
|
+
candidate_dicts.append(
|
|
866
|
+
{
|
|
867
|
+
"file": g_file,
|
|
868
|
+
"symbol": g_name,
|
|
869
|
+
"canonical_id": g_cid,
|
|
870
|
+
"start_line": int(g_row["start_line"] or 1),
|
|
871
|
+
"end_line": int(g_row["end_line"] or 1),
|
|
872
|
+
"score": 0.88,
|
|
873
|
+
"relationship": "DEFINES",
|
|
874
|
+
"snippet": f"Gateway method {g_sym} in {g_file}",
|
|
875
|
+
}
|
|
876
|
+
)
|
|
877
|
+
except Exception:
|
|
878
|
+
pass
|
|
879
|
+
|
|
880
|
+
# Seed direct methods of resolved class targets
|
|
881
|
+
for res in _target_resolutions.values():
|
|
882
|
+
cls_qname = res.qualified_name
|
|
883
|
+
if cls_qname and res.target_type.value == "CLASS":
|
|
884
|
+
try:
|
|
885
|
+
m_rows = con.execute(
|
|
886
|
+
"SELECT canonical_id, qualified_name, name, kind, path, start_line, end_line "
|
|
887
|
+
"FROM symbols WHERE qualified_name LIKE ?",
|
|
888
|
+
(f"{cls_qname}.%",),
|
|
889
|
+
).fetchall()
|
|
890
|
+
for m_row in m_rows:
|
|
891
|
+
m_name = str(m_row["name"])
|
|
892
|
+
m_sym = str(m_row["qualified_name"] or m_name)
|
|
893
|
+
m_cid = str(m_row["canonical_id"] or "")
|
|
894
|
+
m_file = str(m_row["path"] or "")
|
|
895
|
+
if (
|
|
896
|
+
_guard.allows_symbol(m_sym)
|
|
897
|
+
and _guard.allows_symbol(m_name)
|
|
898
|
+
and (not m_cid or _guard.allows_canonical_id(m_cid))
|
|
899
|
+
and _guard.allows_file(m_file)
|
|
900
|
+
):
|
|
901
|
+
target_symbols.add(m_name)
|
|
902
|
+
target_symbols.add(m_sym)
|
|
903
|
+
candidate_dicts.append(
|
|
904
|
+
{
|
|
905
|
+
"file": m_file,
|
|
906
|
+
"symbol": m_name,
|
|
907
|
+
"canonical_id": m_cid,
|
|
908
|
+
"start_line": int(m_row["start_line"] or 1),
|
|
909
|
+
"end_line": int(m_row["end_line"] or 1),
|
|
910
|
+
"score": 0.88,
|
|
911
|
+
"relationship": "DEFINES",
|
|
912
|
+
"snippet": f"Method {m_sym} of target class {cls_qname}",
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
except Exception:
|
|
916
|
+
pass
|
|
804
917
|
|
|
805
918
|
# 5. Graph exploration (callers/callees) & Tests with deterministic seeds
|
|
806
919
|
t_graph_start = time.perf_counter()
|